diff --git a/Cargo.lock b/Cargo.lock index 62fa3da20..e1b1880b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2278,6 +2278,8 @@ dependencies = [ "semver 1.0.27", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/examples/auth-component-no-auth/Cargo.lock b/examples/auth-component-no-auth/Cargo.lock index a3106a54a..1347cb036 100644 --- a/examples/auth-component-no-auth/Cargo.lock +++ b/examples/auth-component-no-auth/Cargo.lock @@ -880,6 +880,8 @@ dependencies = [ "semver 1.0.26", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/examples/auth-component-rpo-falcon512/Cargo.lock b/examples/auth-component-rpo-falcon512/Cargo.lock index 69b9a03c1..f0f96bb0a 100644 --- a/examples/auth-component-rpo-falcon512/Cargo.lock +++ b/examples/auth-component-rpo-falcon512/Cargo.lock @@ -881,6 +881,8 @@ dependencies = [ "semver 1.0.27", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/examples/basic-wallet-tx-script/Cargo.lock b/examples/basic-wallet-tx-script/Cargo.lock index a732370d1..b61a55f38 100644 --- a/examples/basic-wallet-tx-script/Cargo.lock +++ b/examples/basic-wallet-tx-script/Cargo.lock @@ -873,6 +873,8 @@ dependencies = [ "semver 1.0.26", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/examples/basic-wallet-tx-script/src/lib.rs b/examples/basic-wallet-tx-script/src/lib.rs index 469d59a88..d33761cef 100644 --- a/examples/basic-wallet-tx-script/src/lib.rs +++ b/examples/basic-wallet-tx-script/src/lib.rs @@ -10,7 +10,7 @@ use miden::{intrinsics::advice::adv_push_mapvaln, *}; -use crate::bindings::miden::basic_wallet::basic_wallet; +use crate::bindings::Account; // Input layout constants const TAG_INDEX: usize = 0; @@ -23,7 +23,7 @@ const ASSET_START: usize = 8; const ASSET_END: usize = 12; #[tx_script] -fn run(arg: Word) { +fn run(arg: Word, account: Account) { let num_felts = adv_push_mapvaln(arg.clone()); let num_felts_u64 = num_felts.as_u64(); assert_eq(Felt::from_u32((num_felts_u64 % 4) as u32), felt!(0)); @@ -35,7 +35,8 @@ fn run(arg: Word) { let note_type = input[NOTE_TYPE_INDEX]; let execution_hint = input[EXECUTION_HINT_INDEX]; let recipient: [Felt; 4] = input[RECIPIENT_START..RECIPIENT_END].try_into().unwrap(); - let note_idx = output_note::create(tag.into(), aux, note_type.into(), execution_hint, recipient.into()); + let note_idx = + output_note::create(tag.into(), aux, note_type.into(), execution_hint, recipient.into()); let asset: [Felt; 4] = input[ASSET_START..ASSET_END].try_into().unwrap(); - basic_wallet::move_asset_to_note(asset.into(), note_idx); + account.move_asset_to_note(asset.into(), note_idx); } diff --git a/examples/basic-wallet/Cargo.lock b/examples/basic-wallet/Cargo.lock index b7b698953..9b9ed7fe5 100644 --- a/examples/basic-wallet/Cargo.lock +++ b/examples/basic-wallet/Cargo.lock @@ -873,6 +873,8 @@ dependencies = [ "semver 1.0.26", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/examples/counter-contract/Cargo.lock b/examples/counter-contract/Cargo.lock index 94ff57c22..e6f8437f5 100644 --- a/examples/counter-contract/Cargo.lock +++ b/examples/counter-contract/Cargo.lock @@ -873,6 +873,8 @@ dependencies = [ "semver 1.0.26", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/examples/counter-note/Cargo.lock b/examples/counter-note/Cargo.lock index 6ebef5a68..9d5bc2547 100644 --- a/examples/counter-note/Cargo.lock +++ b/examples/counter-note/Cargo.lock @@ -873,6 +873,8 @@ dependencies = [ "semver 1.0.26", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/examples/p2id-note/Cargo.lock b/examples/p2id-note/Cargo.lock index c5530be26..8739e9261 100644 --- a/examples/p2id-note/Cargo.lock +++ b/examples/p2id-note/Cargo.lock @@ -866,6 +866,8 @@ dependencies = [ "semver 1.0.26", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/examples/p2id-note/src/lib.rs b/examples/p2id-note/src/lib.rs index dcf57ef77..14aecef8a 100644 --- a/examples/p2id-note/src/lib.rs +++ b/examples/p2id-note/src/lib.rs @@ -9,10 +9,10 @@ use miden::*; -use crate::bindings::miden::basic_wallet::basic_wallet::receive_asset; +use crate::bindings::Account; #[note_script] -fn run(_arg: Word) { +fn run(_arg: Word, account: Account) { let inputs = active_note::get_inputs(); let target_account_id_prefix = inputs[0]; let target_account_id_suffix = inputs[1]; @@ -23,6 +23,6 @@ fn run(_arg: Word) { let assets = active_note::get_assets(); for asset in assets { - receive_asset(asset); + account.receive_asset(asset); } } diff --git a/examples/p2ide-note/Cargo.lock b/examples/p2ide-note/Cargo.lock index 5dc6b40e9..3c5c6af45 100644 --- a/examples/p2ide-note/Cargo.lock +++ b/examples/p2ide-note/Cargo.lock @@ -1064,6 +1064,8 @@ dependencies = [ "semver 1.0.26", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/examples/storage-example/Cargo.lock b/examples/storage-example/Cargo.lock index 60d375101..10fdfef34 100644 --- a/examples/storage-example/Cargo.lock +++ b/examples/storage-example/Cargo.lock @@ -866,6 +866,8 @@ dependencies = [ "semver 1.0.26", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/sdk/base-macros/Cargo.toml b/sdk/base-macros/Cargo.toml index d25ecb2db..ec6813973 100644 --- a/sdk/base-macros/Cargo.toml +++ b/sdk/base-macros/Cargo.toml @@ -22,6 +22,8 @@ semver.workspace = true toml.workspace = true syn.workspace = true heck.workspace = true +wit-bindgen-core = "0.46" +wit-bindgen-rust = { version = "0.46", default-features = false } [dev-dependencies] # Use local paths for dev-only dependency to avoid relying on crates.io during packaging diff --git a/sdk/base-macros/src/generate.rs b/sdk/base-macros/src/generate.rs index eca0bff50..bafd83fe8 100644 --- a/sdk/base-macros/src/generate.rs +++ b/sdk/base-macros/src/generate.rs @@ -1,25 +1,51 @@ -use std::{ - env, fs, - io::ErrorKind, - path::{Path, PathBuf}, -}; +use std::{collections::HashMap, env, fs, path::PathBuf}; -use proc_macro2::{Literal, Span, TokenStream as TokenStream2}; -use quote::quote; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{quote, ToTokens}; use syn::{ parse::{Parse, ParseStream}, - Error, LitStr, Token, + parse_quote, + spanned::Spanned, + visit_mut::VisitMut, + Attribute, Error, File, FnArg, ImplItem, ImplItemFn, Item, ItemFn, ItemImpl, ItemStruct, + LitStr, Pat, ReturnType, Token, TypePath, }; +use wit_bindgen_core::wit_parser::{PackageId, Resolve, UnresolvedPackageGroup}; +use wit_bindgen_rust::{Opts, WithOption}; + +use crate::manifest_paths; -/// File name for the embedded Miden SDK WIT . -const SDK_WIT_FILE_NAME: &str = "miden.wit"; -/// Embedded Miden SDK WIT source. -pub(crate) const SDK_WIT_SOURCE: &str = include_str!("../wit/miden.wit"); +/// Name of the wrapper struct generated to aggregate imported interface methods. +const WRAPPER_STRUCT_NAME: &str = "Account"; #[derive(Default)] struct GenerateArgs { inline: Option, - with: Option, + /// Custom `with` entries parsed from the macro input. + /// Each entry maps a WIT interface/type to either `generate` or a Rust path. + /// Stored directly as `(String, WithOption)` to avoid an intermediate representation. + with_entries: Vec<(String, WithOption)>, +} + +/// Parses a single `with` entry like `"miden:foo/bar": generate` or `"miden:foo/bar": ::my::Path`. +fn parse_with_entry(input: ParseStream<'_>) -> syn::Result<(String, WithOption)> { + let key: LitStr = input.parse()?; + input.parse::()?; + let path: syn::Path = input.parse()?; + + // Check if the path is the special `generate` keyword + let option = if path.leading_colon.is_none() + && path.segments.len() == 1 + && path.segments.first().is_some_and(|seg| seg.ident == "generate") + { + WithOption::Generate + } else { + // Convert syn::Path to string, removing spaces for consistency + let path_str = path.to_token_stream().to_string().replace(' ', ""); + WithOption::Path(path_str) + }; + + Ok((key.value(), option)) } impl Parse for GenerateArgs { @@ -37,13 +63,18 @@ impl Parse for GenerateArgs { } args.inline = Some(input.parse()?); } else if name == "with" { - if args.with.is_some() { + if !args.with_entries.is_empty() { return Err(syn::Error::new(ident.span(), "duplicate `with` argument")); } let content; syn::braced!(content in input); - let tokens = content.parse::()?; - args.with = Some(tokens); + // Parse comma-separated with entries directly into (String, WithOption) pairs + while !content.is_empty() { + args.with_entries.push(parse_with_entry(&content)?); + if content.peek(Token![,]) { + content.parse::()?; + } + } } else { return Err(syn::Error::new( ident.span(), @@ -88,12 +119,6 @@ pub(crate) fn expand(input: proc_macro::TokenStream) -> proc_macro::TokenStream .into(); } - let path_literals: Vec<_> = - config.paths.iter().map(|path| Literal::string(path)).collect(); - - let inline_clause = args.inline.as_ref().map(|src| quote! { inline: #src, }); - let custom_with_entries = args.with.unwrap_or_else(TokenStream2::new); - let inline_world = args .inline .as_ref() @@ -109,334 +134,1034 @@ pub(crate) fn expand(input: proc_macro::TokenStream) -> proc_macro::TokenStream .into(); } - let world_clause = world_value.as_ref().map(|world| { - let literal = Literal::string(world); - quote! { world: #literal, } - }); - - quote! { - // Wrap the bindings in the `bindings` module since `generate!` makes a top level - // module named after the package namespace which is `miden` for all our projects - // so its conflicts with the `miden` crate (SDK) - #[doc(hidden)] - #[allow(dead_code)] - pub mod bindings { - ::miden::wit_bindgen::generate!({ - #inline_clause - #world_clause - path: [#(#path_literals),*], - generate_all, - runtime_path: "::miden::wit_bindgen::rt", - // path to use in the generated `export!` macro - default_bindings_module: "bindings", - with: { - #custom_with_entries - "miden:base/core-types@1.0.0": generate, - "miden:base/core-types@1.0.0/felt": ::miden::Felt, - "miden:base/core-types@1.0.0/word": ::miden::Word, - "miden:base/core-types@1.0.0/asset": ::miden::Asset, - "miden:base/core-types@1.0.0/account-id": ::miden::AccountId, - "miden:base/core-types@1.0.0/tag": ::miden::Tag, - "miden:base/core-types@1.0.0/note-type": ::miden::NoteType, - "miden:base/core-types@1.0.0/recipient": ::miden::Recipient, - "miden:base/core-types@1.0.0/note-idx": ::miden::NoteIdx, - }, - }); - } + match generate_bindings(&args, &config, world_value.as_deref()) { + Ok(raw_bindings) => match augment_generated_bindings(raw_bindings) { + Ok(augmented) => { + quote! { + // Wrap the bindings in the `bindings` module since `generate!` makes a top level + // module named after the package namespace which is `miden` for all our projects + // so it conflicts with the `miden` crate (SDK) + #[doc(hidden)] + #[allow(dead_code)] + pub mod bindings { + #augmented + } + } + .into() + } + Err(err) => err.to_compile_error().into(), + }, + Err(err) => err.to_compile_error().into(), } - .into() } Err(err) => err.to_compile_error().into(), } } -mod manifest_paths { - use toml::Value; +/// Generates WIT bindings using `wit-bindgen` directly instead of the `generate!` macro. +/// +/// The `world` parameter specifies which world to generate bindings for. This should already +/// be resolved by the caller (either from inline WIT or from the local wit/ directory). +/// If `None`, wit-bindgen will attempt to select a default world from the loaded packages. +fn generate_bindings( + args: &GenerateArgs, + config: &manifest_paths::ResolvedWit, + world: Option<&str>, +) -> Result { + let inline_src = args.inline.as_ref().map(|src| src.value()); + let inline_ref = inline_src.as_deref(); + let wit_sources = load_wit_sources(&config.paths, inline_ref)?; - use super::*; - use crate::util::bundled_wit_folder; + let world_id = wit_sources + .resolve + .select_world(&wit_sources.packages, world) + .map_err(|err| Error::new(Span::call_site(), err.to_string()))?; - /// WIT metadata extracted from the consuming crate. - pub struct ResolvedWit { - pub paths: Vec, - pub world: Option, - } + let mut opts = Opts { + generate_all: true, + runtime_path: Some("::miden::wit_bindgen::rt".to_string()), + default_bindings_module: Some("bindings".to_string()), + ..Opts::default() + }; + push_custom_with_entries(&mut opts, &args.with_entries); + push_default_with_entries(&mut opts); - #[derive(Default)] - pub struct ResolveOptions { - pub allow_missing_local_wit: bool, - } + let mut generated_files = wit_bindgen_core::Files::default(); + let mut generator = opts.build(); + generator + .generate(&wit_sources.resolve, world_id, &mut generated_files) + .map_err(|err| Error::new(Span::call_site(), err.to_string()))?; - /// Collects WIT search paths and the target world from `Cargo.toml` + local files. - pub fn resolve_wit_paths(options: ResolveOptions) -> Result { - let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|err| { - Error::new(Span::call_site(), format!("failed to read CARGO_MANIFEST_DIR: {err}")) - })?; - let manifest_path = Path::new(&manifest_dir).join("Cargo.toml"); + let (_, src_bytes) = generated_files + .iter() + .next() + .ok_or_else(|| Error::new(Span::call_site(), "wit-bindgen emitted no bindings"))?; + let src = std::str::from_utf8(src_bytes) + .map_err(|err| Error::new(Span::call_site(), format!("invalid UTF-8: {err}")))?; + let mut tokens: TokenStream2 = src + .parse() + .map_err(|err| Error::new(Span::call_site(), format!("failed to parse bindings: {err}")))?; - let manifest_content = fs::read_to_string(&manifest_path).map_err(|err| { + // Include a dummy `include_bytes!` for any files we read so rustc knows that + // we depend on the contents of those files. + for path in wit_sources.files_read { + let utf8_path = path.to_str().ok_or_else(|| { Error::new( Span::call_site(), - format!("failed to read manifest '{}': {err}", manifest_path.display()), + format!("path '{}' contains invalid UTF-8", path.display()), ) })?; + tokens.extend(quote! { + const _: &[u8] = include_bytes!(#utf8_path); + }); + } - let manifest: Value = manifest_content.parse().map_err(|err| { - Error::new( - Span::call_site(), - format!("failed to parse manifest '{}': {err}", manifest_path.display()), - ) - })?; + Ok(tokens) +} - let canonical_prelude_dir = ensure_sdk_wit()?; +/// Post-processes wit-bindgen output to inject wrapper structs for imported interfaces. +/// +/// This transforms the raw bindings by walking all modules and injecting an `Account` wrapper +/// struct at the bindings root level. The struct has methods that delegate to the generated +/// free functions in leaf modules. This provides a more ergonomic API +/// (e.g., `Account::default().receive_asset(asset)` instead of +/// `miden::basic_wallet::basic_wallet::receive_asset(asset)`). +/// +/// ## Leaf Module Selection +/// +/// Only "leaf" modules (those containing no nested modules) contribute methods to the +/// wrapper struct. This is because wit-bindgen generates a module hierarchy where: +/// - Non-leaf modules represent WIT package namespaces (e.g., `miden::basic_wallet`) +/// - Leaf modules represent actual WIT interfaces with callable functions +/// +/// For example, given the module structure: +/// ```text +/// miden/ +/// basic_wallet/ +/// basic_wallet/ <- leaf module, methods collected here +/// receive_asset() +/// send_asset() +/// ``` +/// Only functions from `miden::basic_wallet::basic_wallet` are wrapped. +fn augment_generated_bindings(tokens: TokenStream2) -> syn::Result { + let mut file: File = syn::parse2(tokens)?; + let mut collected_methods = Vec::new(); + collect_wrapper_methods(&file.items, &mut Vec::new(), &mut collected_methods)?; - let mut resolved = Vec::new(); + // Check for method name collisions across different interfaces + check_method_name_collisions(&collected_methods)?; - let prelude_dir = canonical_prelude_dir - .to_str() - .ok_or_else(|| { - Error::new( - Span::call_site(), - format!("path '{}' contains invalid UTF-8", canonical_prelude_dir.display()), - ) - })? - .to_owned(); - - resolved.push(prelude_dir); - - if let Some(dependencies) = manifest - .get("package") - .and_then(Value::as_table) - .and_then(|package| package.get("metadata")) - .and_then(Value::as_table) - .and_then(|metadata| metadata.get("component")) - .and_then(Value::as_table) - .and_then(|component| component.get("target")) - .and_then(Value::as_table) - .and_then(|target| target.get("dependencies")) - .and_then(Value::as_table) - { - for (name, dependency) in dependencies.iter() { - let table = dependency.as_table().ok_or_else(|| { - Error::new( - Span::call_site(), - format!( - "dependency '{name}' under \ - [package.metadata.component.target.dependencies] must be a table" - ), - ) - })?; - - let path_value = table.get("path").and_then(Value::as_str).ok_or_else(|| { - Error::new( - Span::call_site(), - format!("dependency '{name}' is missing a 'path' entry"), - ) - })?; - - let raw_path = PathBuf::from(path_value); - let absolute = if raw_path.is_absolute() { - raw_path - } else { - Path::new(&manifest_dir).join(&raw_path) - }; - - let canonical = fs::canonicalize(&absolute).unwrap_or_else(|_| absolute.clone()); - - let metadata = fs::metadata(&canonical).map_err(|err| { - Error::new( - Span::call_site(), - format!( - "failed to read metadata for dependency '{name}' path '{}': {err}", - canonical.display() - ), - ) - })?; - - let search_path = if metadata.is_dir() { - canonical - } else if let Some(parent) = canonical.parent() { - parent.to_path_buf() - } else { - return Err(Error::new( - Span::call_site(), - format!( - "dependency '{name}' path '{}' does not have a parent directory", - canonical.display() - ), - )); - }; - - let path_str = search_path.to_str().ok_or_else(|| { - Error::new( - Span::call_site(), - format!("dependency '{name}' path contains invalid UTF-8"), - ) - })?; - - if !resolved.iter().any(|existing| existing == path_str) { - resolved.push(path_str.to_owned()); - } + if !collected_methods.is_empty() { + let struct_ident = syn::Ident::new(WRAPPER_STRUCT_NAME, Span::call_site()); + let struct_item: ItemStruct = parse_quote! { + /// Wrapper struct providing methods that delegate to imported interface functions. + #[derive(Clone, Copy, Default)] + pub struct #struct_ident; + }; + + let mut impl_item: ItemImpl = parse_quote! { + impl #struct_ident {} + }; + impl_item + .items + .extend(collected_methods.into_iter().map(|cm| ImplItem::Fn(cm.method))); + + file.items.push(Item::Struct(struct_item)); + file.items.push(Item::Impl(impl_item)); + } + + Ok(file.into_token_stream()) +} + +/// Result of loading and parsing WIT sources from file paths and optional inline content. +struct LoadedWitSources { + /// The resolved WIT definitions containing all types, interfaces, and worlds. + resolve: Resolve, + /// Package IDs to use for world selection. When inline source is provided, this contains + /// only the inline package; otherwise it contains all packages from file paths. + packages: Vec, + /// File paths that were read during WIT parsing. Used to generate dummy `include_bytes!` + /// calls so rustc knows to recompile when these files change. + files_read: Vec, +} + +/// Loads WIT sources from file paths and optionally an inline source. +fn load_wit_sources( + paths: &[String], + inline_source: Option<&str>, +) -> Result { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|err| { + Error::new(Span::call_site(), format!("failed to read CARGO_MANIFEST_DIR: {err}")) + })?; + let manifest_dir = PathBuf::from(manifest_dir); + + let mut resolve = Resolve::default(); + let mut packages = Vec::new(); + let mut files = Vec::new(); + + // Load WIT definitions from file paths. These are always loaded to populate the resolver + // with type definitions that the inline source may depend on. + for path in paths { + let path_buf = PathBuf::from(path); + let absolute = if path_buf.is_absolute() { + path_buf + } else { + manifest_dir.join(path_buf) + }; + let normalized = fs::canonicalize(&absolute).unwrap_or(absolute); + let (pkg, sources) = resolve + .push_path(normalized.clone()) + .map_err(|err| Error::new(Span::call_site(), err.to_string()))?; + packages.push(pkg); + files.extend(sources.paths().map(|p| p.to_owned())); + } + + if let Some(src) = inline_source { + // When inline source is provided, it becomes the primary package for world selection. + // We clear previously collected package IDs because the inline source defines the world + // we want to generate bindings for. The file-based packages are still loaded above and + // remain in the resolver - they provide type definitions that the inline world imports. + packages.clear(); + let group = UnresolvedPackageGroup::parse("inline", src) + .map_err(|err| Error::new(Span::call_site(), err.to_string()))?; + let pkg = resolve + .push_group(group) + .map_err(|err| Error::new(Span::call_site(), err.to_string()))?; + packages.push(pkg); + } + + Ok(LoadedWitSources { + resolve, + packages, + files_read: files, + }) +} + +/// Pushes user-provided `with` entries to the wit-bindgen options. +fn push_custom_with_entries(opts: &mut Opts, entries: &[(String, WithOption)]) { + opts.with.extend(entries.iter().cloned()); +} + +/// Pushes default `with` entries that map Miden base types to SDK types. +fn push_default_with_entries(opts: &mut Opts) { + opts.with + .push(("miden:base/core-types@1.0.0".to_string(), WithOption::Generate)); + push_path_entry(opts, "miden:base/core-types@1.0.0/felt", "::miden::Felt"); + push_path_entry(opts, "miden:base/core-types@1.0.0/word", "::miden::Word"); + push_path_entry(opts, "miden:base/core-types@1.0.0/asset", "::miden::Asset"); + push_path_entry(opts, "miden:base/core-types@1.0.0/account-id", "::miden::AccountId"); + push_path_entry(opts, "miden:base/core-types@1.0.0/tag", "::miden::Tag"); + push_path_entry(opts, "miden:base/core-types@1.0.0/note-type", "::miden::NoteType"); + push_path_entry(opts, "miden:base/core-types@1.0.0/recipient", "::miden::Recipient"); + push_path_entry(opts, "miden:base/core-types@1.0.0/note-idx", "::miden::NoteIdx"); +} + +fn push_path_entry(opts: &mut Opts, key: &str, value: &str) { + opts.with.push((key.to_string(), WithOption::Path(value.to_string()))); +} + +/// A collected wrapper method along with its source module path. +struct CollectedMethod { + method: ImplItemFn, + /// The module path where this method originated (e.g., "miden::basic_wallet::basic_wallet"). + source_path: String, +} + +/// Recursively walks all modules and collects wrapper methods from leaf modules. +/// +/// The `path` parameter tracks the current module path for generating correct call paths. +/// Collected methods are appended to `methods_out` and will be placed in the root `Account` struct. +fn collect_wrapper_methods( + items: &[Item], + path: &mut Vec, + methods_out: &mut Vec, +) -> syn::Result<()> { + for item in items.iter() { + if let Item::Mod(module) = item { + path.push(module.ident.clone()); + if let Some((_, ref content)) = module.content { + collect_wrapper_methods(content, path, methods_out)?; + collect_methods_from_module(content, path, methods_out)?; } + path.pop(); } + } - let local_wit_root = Path::new(&manifest_dir).join("wit"); - let mut world = None; + Ok(()) +} - if local_wit_root.exists() && !options.allow_missing_local_wit { - let local_root = fs::canonicalize(&local_wit_root).unwrap_or(local_wit_root); - let local_root_str = local_root.to_str().ok_or_else(|| { - Error::new( - Span::call_site(), - format!("path '{}' contains invalid UTF-8", local_root.display()), - ) - })?; - if !resolved.iter().any(|existing| existing == local_root_str) { - resolved.push(local_root_str.to_owned()); +/// Collects wrapper methods from a leaf module's public functions. +/// +/// A leaf module is one that contains no nested modules. Only leaf modules contribute +/// methods, as non-leaf modules typically represent namespace hierarchy rather than +/// concrete interfaces. +fn collect_methods_from_module( + items: &[Item], + path: &[syn::Ident], + methods_out: &mut Vec, +) -> syn::Result<()> { + if !should_generate_struct(path, items) { + return Ok(()); + } + + let functions: Vec<&ItemFn> = items + .iter() + .filter_map(|item| match item { + Item::Fn(func) if is_target_function(func) => Some(func), + _ => None, + }) + .collect(); + + let source_path = format_module_path(path); + for func in functions { + methods_out.push(CollectedMethod { + method: build_wrapper_method(func, path)?, + source_path: source_path.clone(), + }); + } + + Ok(()) +} + +/// Builds a wrapper method that delegates to the original free function. +/// +/// Type paths in the signature are qualified with the module path prefix so they +/// resolve correctly when the method is placed at the bindings root level. +fn build_wrapper_method(func: &ItemFn, module_path: &[syn::Ident]) -> syn::Result { + let mut sig = func.sig.clone(); + sig.inputs.insert(0, parse_quote!(&self)); + + // Qualify type paths in the signature so they resolve from the bindings root + qualify_signature_types(&mut sig, module_path); + + let arg_idents = collect_arg_idents(func)?; + let call_expr = wrapper_call_tokens(module_path, &sig.ident, &arg_idents); + + let method_doc = format!("Calls `{}` from `{}`.", sig.ident, format_module_path(module_path)); + let doc_attr: Attribute = parse_quote!(#[doc = #method_doc]); + let inline_attr: Attribute = parse_quote!(#[inline(always)]); + + let body_tokens = match &sig.output { + ReturnType::Default => quote!({ #call_expr; }), + _ => quote!({ #call_expr }), + }; + let block = syn::parse2(body_tokens)?; + + Ok(ImplItemFn { + attrs: vec![doc_attr, inline_attr], + vis: func.vis.clone(), + defaultness: None, + sig, + block, + }) +} + +/// Qualifies type paths in a function signature with the module path prefix. +/// +/// This transforms simple type names (e.g., `StructA`) into fully qualified paths +/// (e.g., `miden::component::component::StructA`) so they resolve correctly when +/// the method is placed at the bindings root level. +fn qualify_signature_types(sig: &mut syn::Signature, module_path: &[syn::Ident]) { + struct TypeQualifier<'a> { + module_path: &'a [syn::Ident], + } + + impl VisitMut for TypeQualifier<'_> { + fn visit_type_path_mut(&mut self, type_path: &mut TypePath) { + // Only qualify paths that: + // 1. Don't already have a leading colon (not absolute like `::foo`) + // 2. Are simple single-segment paths (like `StructA`, not `foo::Bar`) + // 3. Don't start with common primitive/std type names + if type_path.qself.is_none() + && type_path.path.leading_colon.is_none() + && type_path.path.segments.len() == 1 + { + let first_segment = &type_path.path.segments[0].ident; + let name = first_segment.to_string(); + + // Skip primitive types and common std types + if is_primitive_or_std_type(&name) { + return; + } + + // Build the qualified path: module_path::TypeName + let mut new_segments = syn::punctuated::Punctuated::new(); + for ident in self.module_path { + new_segments.push(syn::PathSegment { + ident: ident.clone(), + arguments: syn::PathArguments::None, + }); + } + // Add the original type segment (preserving generics) + new_segments.push(type_path.path.segments[0].clone()); + + type_path.path.segments = new_segments; } - world = detect_world_name(&local_root)?; + + // Continue visiting nested types (e.g., generics) + syn::visit_mut::visit_type_path_mut(self, type_path); } + } - Ok(ResolvedWit { - paths: resolved, - world, + let mut qualifier = TypeQualifier { module_path }; + qualifier.visit_signature_mut(sig); +} + +/// Returns true if the name is a primitive type or common std type that shouldn't be qualified. +/// +/// This list covers Rust primitives and common standard library types. WIT-generated bindings +/// only use a subset of these (primitives, String, Vec, Option, Result), but we include +/// additional common types for safety. Types like `Rc`, `Arc`, `RefCell` are not used by +/// wit-bindgen and are intentionally omitted. +fn is_primitive_or_std_type(name: &str) -> bool { + matches!( + name, + "bool" + | "char" + | "str" + | "u8" + | "u16" + | "u32" + | "u64" + | "u128" + | "usize" + | "i8" + | "i16" + | "i32" + | "i64" + | "i128" + | "isize" + | "f32" + | "f64" + | "String" + | "Vec" + | "Option" + | "Result" + | "Self" + ) +} + +/// Extracts argument identifiers from a function signature. +/// +/// Returns an error if the function contains a receiver (`self`) or uses +/// unsupported argument patterns (e.g., destructuring patterns). +fn collect_arg_idents(func: &ItemFn) -> syn::Result> { + func.sig + .inputs + .iter() + .map(|arg| match arg { + FnArg::Receiver(_) => { + Err(Error::new(func.sig.ident.span(), "unexpected receiver in generated function")) + } + FnArg::Typed(pat_type) => match pat_type.pat.as_ref() { + Pat::Ident(pat_ident) => Ok(pat_ident.ident.clone()), + other => Err(Error::new( + other.span(), + format!( + "unsupported argument pattern `{}` in generated function", + quote!(#other) + ), + )), + }, }) + .collect() +} + +/// Generates tokens for calling the original free function from the wrapper method. +fn wrapper_call_tokens( + module_path: &[syn::Ident], + fn_ident: &syn::Ident, + args: &[syn::Ident], +) -> TokenStream2 { + let mut path_tokens = quote! { crate::bindings }; + for ident in module_path { + path_tokens = quote! { #path_tokens :: #ident }; + } + + quote! { #path_tokens :: #fn_ident(#(#args),*) } +} + +/// Determines whether a wrapper struct should be generated for the given module. +/// +/// Returns `false` for: +/// - Empty paths +/// - `exports` modules (these are user-implemented exports, not imports) +/// - Modules starting with underscore (internal/private modules) +/// - Non-leaf modules (modules that contain nested modules) +fn should_generate_struct(path: &[syn::Ident], items: &[Item]) -> bool { + if path.is_empty() { + return false; + } + let first = path[0].to_string(); + if first == "exports" { + return false; } + if first.starts_with('_') { + return false; + } + let last = path.last().unwrap().to_string(); + if last.starts_with('_') { + return false; + } + // Only generate for leaf modules (no nested modules) + !items.iter().any(|item| matches!(item, Item::Mod(_))) +} - /// Ensures the embedded Miden SDK WIT is materialized in the project's folder. - fn ensure_sdk_wit() -> Result { - let autogenerated_wit_folder = bundled_wit_folder()?; +/// Determines whether a function should have a wrapper method generated. +/// +/// Returns `true` for public, safe functions that don't start with underscore. +fn is_target_function(func: &ItemFn) -> bool { + matches!(func.vis, syn::Visibility::Public(_)) + && func.sig.unsafety.is_none() + && !func.sig.ident.to_string().starts_with('_') +} - let sdk_wit_path = autogenerated_wit_folder.join(super::SDK_WIT_FILE_NAME); - let sdk_version: &str = env!("CARGO_PKG_VERSION"); - let expected_source = format!( - "/// NOTE: This file is auto-generated from the Miden SDK.\n/// Version: \ - v{sdk_version}\n/// Any manual edits will be overwritten.\n\n{SDK_WIT_SOURCE}" - ); - let should_write_wit = match fs::read_to_string(&sdk_wit_path) { - Ok(existing) => existing != expected_source, - Err(err) if err.kind() == ErrorKind::NotFound => true, - Err(err) => { - return Err(Error::new( - Span::call_site(), - format!("failed to read '{}': {err}", sdk_wit_path.display()), - )); - } - }; +/// Formats a module path as a `::` separated string for use in documentation. +fn format_module_path(path: &[syn::Ident]) -> String { + path.iter().map(|ident| ident.to_string()).collect::>().join("::") +} - if should_write_wit { - fs::write(&sdk_wit_path, expected_source).map_err(|err| { - Error::new( - Span::call_site(), - format!("failed to write '{}': {err}", sdk_wit_path.display()), - ) - })?; +/// Checks for method name collisions across collected wrapper methods. +/// +/// If multiple imported interfaces define functions with the same name, they would all be +/// added to the `Account` struct, causing a compilation error. This function detects such +/// collisions early and provides a clear error message indicating which interfaces conflict. +fn check_method_name_collisions(methods: &[CollectedMethod]) -> syn::Result<()> { + let mut seen: HashMap = HashMap::new(); + + for collected in methods { + let method_name = collected.method.sig.ident.to_string(); + + if let Some(existing_path) = seen.get(&method_name) { + return Err(Error::new( + Span::call_site(), + format!( + "method name collision in generated `{WRAPPER_STRUCT_NAME}` struct: \ + `{method_name}` is defined in both `{existing_path}` and `{}`. Consider \ + using the original module paths directly instead of the wrapper struct.", + collected.source_path + ), + )); } - Ok(fs::canonicalize(&autogenerated_wit_folder).unwrap_or(autogenerated_wit_folder)) + seen.insert(method_name, &collected.source_path); } - /// Scans the component's `wit` directory to find the default world. - fn detect_world_name(wit_root: &Path) -> Result, Error> { - let mut entries = fs::read_dir(wit_root) - .map_err(|err| { - Error::new( - Span::call_site(), - format!("failed to read '{}': {err}", wit_root.display()), - ) - })? - .collect::, _>>() - .map_err(|err| { - Error::new( - Span::call_site(), - format!("failed to iterate '{}': {err}", wit_root.display()), - ) - })?; - entries.sort_by_key(|entry| entry.file_name()); + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Helper to parse Rust source into a syn::File. + fn parse_file(src: &str) -> File { + syn::parse_str(src).unwrap_or_else(|e| panic!("failed to parse test source: {e}\n{src}")) + } + + #[test] + fn test_should_generate_struct_empty_path() { + let empty_items: Vec = vec![]; + assert!(!should_generate_struct(&[], &empty_items)); + } + + #[test] + fn test_should_generate_struct_exports_excluded() { + let empty_items: Vec = vec![]; + let path = vec![syn::Ident::new("exports", Span::call_site())]; + assert!(!should_generate_struct(&path, &empty_items)); - for entry in entries { - let path = entry.path(); - if path.file_name().is_some_and(|name| name == "deps") { - continue; + let path = vec![ + syn::Ident::new("exports", Span::call_site()), + syn::Ident::new("foo", Span::call_site()), + ]; + assert!(!should_generate_struct(&path, &empty_items)); + } + + #[test] + fn test_should_generate_struct_underscore_excluded() { + let empty_items: Vec = vec![]; + let path = vec![syn::Ident::new("_private", Span::call_site())]; + assert!(!should_generate_struct(&path, &empty_items)); + + let path = vec![ + syn::Ident::new("miden", Span::call_site()), + syn::Ident::new("_internal", Span::call_site()), + ]; + assert!(!should_generate_struct(&path, &empty_items)); + } + + #[test] + fn test_should_generate_struct_valid_leaf_modules() { + let empty_items: Vec = vec![]; + let path = vec![syn::Ident::new("miden", Span::call_site())]; + assert!(should_generate_struct(&path, &empty_items)); + + let path = vec![ + syn::Ident::new("miden", Span::call_site()), + syn::Ident::new("basic_wallet", Span::call_site()), + ]; + assert!(should_generate_struct(&path, &empty_items)); + } + + #[test] + fn test_should_generate_struct_non_leaf_excluded() { + let path = vec![syn::Ident::new("miden", Span::call_site())]; + // Items containing a nested module + let items_with_mod: Vec = vec![syn::parse_quote! { mod nested {} }]; + assert!(!should_generate_struct(&path, &items_with_mod)); + + // Items with only functions (leaf module) should be allowed + let items_with_fn: Vec = vec![syn::parse_quote! { pub fn foo() {} }]; + assert!(should_generate_struct(&path, &items_with_fn)); + } + + #[test] + fn test_is_target_function_public() { + let func: ItemFn = syn::parse_quote! { + pub fn receive_asset(asset: u64) {} + }; + assert!(is_target_function(&func)); + } + + #[test] + fn test_is_target_function_private_excluded() { + let func: ItemFn = syn::parse_quote! { + fn private_fn() {} + }; + assert!(!is_target_function(&func)); + } + + #[test] + fn test_is_target_function_unsafe_excluded() { + let func: ItemFn = syn::parse_quote! { + pub unsafe fn unsafe_fn() {} + }; + assert!(!is_target_function(&func)); + } + + #[test] + fn test_is_target_function_underscore_excluded() { + let func: ItemFn = syn::parse_quote! { + pub fn _internal() {} + }; + assert!(!is_target_function(&func)); + } + + #[test] + fn test_format_module_path() { + let path = vec![ + syn::Ident::new("miden", Span::call_site()), + syn::Ident::new("basic_wallet", Span::call_site()), + ]; + assert_eq!(format_module_path(&path), "miden::basic_wallet"); + } + + #[test] + fn test_format_module_path_empty() { + assert_eq!(format_module_path(&[]), ""); + } + + #[test] + fn test_collect_arg_idents() { + let func: ItemFn = syn::parse_quote! { + pub fn foo(a: u32, b: String, c: Vec) {} + }; + let idents = collect_arg_idents(&func).unwrap(); + let names: Vec<_> = idents.iter().map(|i| i.to_string()).collect(); + assert_eq!(names, vec!["a", "b", "c"]); + } + + #[test] + fn test_collect_arg_idents_empty() { + let func: ItemFn = syn::parse_quote! { + pub fn no_args() {} + }; + let idents = collect_arg_idents(&func).unwrap(); + assert!(idents.is_empty()); + } + + #[test] + fn test_collect_wrapper_methods_from_leaf_module() { + let src = r#" + mod miden { + mod basic_wallet { + mod basic_wallet { + pub fn receive_asset(asset: u64) {} + pub fn send_asset(asset: u64) {} + } + } + } + "#; + let file = parse_file(src); + let mut methods = Vec::new(); + collect_wrapper_methods(&file.items, &mut Vec::new(), &mut methods).unwrap(); + + // Should have collected 2 methods from the leaf module + assert_eq!(methods.len(), 2); + + // Check method names + let method_names: Vec<_> = methods.iter().map(|m| m.method.sig.ident.to_string()).collect(); + assert!(method_names.contains(&"receive_asset".to_string())); + assert!(method_names.contains(&"send_asset".to_string())); + } + + #[test] + fn test_collect_wrapper_methods_skips_exports() { + let src = r#" + mod exports { + mod my_component { + pub fn exported_fn() {} + } + } + "#; + let file = parse_file(src); + let mut methods = Vec::new(); + collect_wrapper_methods(&file.items, &mut Vec::new(), &mut methods).unwrap(); + + // exports module should not contribute any methods + assert!(methods.is_empty()); + } + + #[test] + fn test_collect_wrapper_methods_skips_empty_modules() { + let src = r#" + mod miden { + mod empty_module { + } } - if path.is_dir() { - continue; + "#; + let file = parse_file(src); + let mut methods = Vec::new(); + collect_wrapper_methods(&file.items, &mut Vec::new(), &mut methods).unwrap(); + + // No methods should be collected from empty module + assert!(methods.is_empty()); + } + + #[test] + fn test_qualify_signature_types() { + let func: ItemFn = syn::parse_quote! { + pub fn test_fn(a: StructA, b: u64) -> StructB {} + }; + let path = vec![ + syn::Ident::new("miden", Span::call_site()), + syn::Ident::new("component", Span::call_site()), + ]; + let method = build_wrapper_method(&func, &path).unwrap(); + + // Check that the types are qualified + let sig_str = method.sig.to_token_stream().to_string(); + assert!(sig_str.contains("miden :: component :: StructA")); + assert!(sig_str.contains("miden :: component :: StructB")); + // Primitives should not be qualified + assert!(sig_str.contains("u64")); + assert!(!sig_str.contains("miden :: component :: u64")); + } + + #[test] + fn test_build_wrapper_method_signature() { + let func: ItemFn = syn::parse_quote! { + pub fn receive_asset(asset: u64) {} + }; + let path = vec![ + syn::Ident::new("miden", Span::call_site()), + syn::Ident::new("basic_wallet", Span::call_site()), + ]; + let method = build_wrapper_method(&func, &path).unwrap(); + + // Method should have &self as first parameter + assert_eq!(method.sig.inputs.len(), 2); + assert!(matches!(method.sig.inputs.first(), Some(FnArg::Receiver(_)))); + + // Should be public + assert!(matches!(method.vis, syn::Visibility::Public(_))); + + // Should have inline attribute + assert!(method.attrs.iter().any(|attr| { attr.path().is_ident("inline") })); + } + + #[test] + fn test_build_wrapper_method_with_return_type() { + let func: ItemFn = syn::parse_quote! { + pub fn get_value() -> u32 { 42 } + }; + let path = vec![syn::Ident::new("test_mod", Span::call_site())]; + let method = build_wrapper_method(&func, &path).unwrap(); + + // Return type should be preserved + assert!(matches!(method.sig.output, ReturnType::Type(_, _))); + } + + #[test] + fn test_augment_generated_bindings_adds_account_struct() { + let src = r#" + mod miden { + mod basic_wallet { + mod basic_wallet { + pub fn receive_asset(asset: u64) {} + pub fn send_asset(to: u32, amount: u64) -> bool { true } + } + } } - if path.extension().and_then(|ext| ext.to_str()) != Some("wit") { - continue; + "#; + let tokens: TokenStream2 = src.parse().unwrap(); + let result = augment_generated_bindings(tokens).unwrap(); + let result_str = result.to_string(); + + // Should contain the Account struct + assert!(result_str.contains("struct Account")); + assert!(result_str.contains("impl Account")); + + // Should contain wrapper methods + assert!(result_str.contains("fn receive_asset")); + assert!(result_str.contains("fn send_asset")); + + // Methods should have &self parameter + assert!(result_str.contains("& self")); + } + + #[test] + fn test_augment_generated_bindings_empty_input() { + let src = ""; + let tokens: TokenStream2 = src.parse().unwrap(); + let result = augment_generated_bindings(tokens).unwrap(); + let result_str = result.to_string(); + + // Should not add Account struct when there are no methods + assert!(!result_str.contains("struct Account")); + } + + #[test] + fn test_augment_generated_bindings_exports_only() { + let src = r#" + mod exports { + mod my_component { + pub fn exported_fn() {} + } } + "#; + let tokens: TokenStream2 = src.parse().unwrap(); + let result = augment_generated_bindings(tokens).unwrap(); + let result_str = result.to_string(); - if let Some((package, world)) = parse_package_and_world(&path)? { - return Ok(Some(format!("{package}/{world}"))); + // Should not add Account struct for exports-only bindings + assert!(!result_str.contains("struct Account")); + } + + #[test] + fn test_augment_generated_bindings_preserves_original_modules() { + let src = r#" + mod miden { + mod wallet { + pub fn get_balance() -> u64 { 0 } + } } - } + "#; + let tokens: TokenStream2 = src.parse().unwrap(); + let result = augment_generated_bindings(tokens).unwrap(); + let result_str = result.to_string(); - Ok(None) + // Original module structure should be preserved + assert!(result_str.contains("mod miden")); + assert!(result_str.contains("mod wallet")); + assert!(result_str.contains("fn get_balance")); } - /// Parses a WIT source file for its package declaration and first world definition. - fn parse_package_and_world(path: &Path) -> Result, Error> { - let contents = fs::read_to_string(path).map_err(|err| { - Error::new( - Span::call_site(), - format!("failed to read WIT file '{}': {err}", path.display()), - ) - })?; + #[test] + fn test_wrapper_call_tokens_generates_correct_path() { + let path = vec![ + syn::Ident::new("miden", Span::call_site()), + syn::Ident::new("basic_wallet", Span::call_site()), + ]; + let fn_ident = syn::Ident::new("receive_asset", Span::call_site()); + let args = vec![syn::Ident::new("asset", Span::call_site())]; - let package = extract_package_name(&contents); - let world = extract_world_name(&contents); + let tokens = wrapper_call_tokens(&path, &fn_ident, &args); + let result = tokens.to_string(); - match (package, world) { - (Some(package), Some(world)) => Ok(Some((package, world))), - _ => Ok(None), + assert!(result.contains("crate :: bindings :: miden :: basic_wallet :: receive_asset")); + assert!(result.contains("asset")); + } + + #[test] + fn test_parse_with_entry_generate() { + let input: TokenStream2 = quote! { "miden:foo/bar": generate }; + let parsed = syn::parse2::(quote! { with = { #input } }).unwrap(); + + assert_eq!(parsed.with_entries.len(), 1); + assert_eq!(parsed.with_entries[0].0, "miden:foo/bar"); + assert!(matches!(parsed.with_entries[0].1, WithOption::Generate)); + } + + #[test] + fn test_parse_with_entry_path() { + let input: TokenStream2 = quote! { "miden:foo/bar": ::my::custom::Type }; + let parsed = syn::parse2::(quote! { with = { #input } }).unwrap(); + + assert_eq!(parsed.with_entries.len(), 1); + assert_eq!(parsed.with_entries[0].0, "miden:foo/bar"); + match &parsed.with_entries[0].1 { + WithOption::Path(p) => assert_eq!(p, "::my::custom::Type"), + _ => panic!("expected Path variant"), } } - /// Returns the package identifier from a WIT source string, if present. - fn extract_package_name(contents: &str) -> Option { - for line in contents.lines() { - let trimmed = strip_comment(line).trim_start(); - if let Some(rest) = trimmed.strip_prefix("package ") { - let mut token = rest.trim(); - if let Some(idx) = token.find(';') { - token = &token[..idx]; + #[test] + fn test_parse_multiple_with_entries() { + let parsed = syn::parse2::(quote! { + with = { + "miden:a/b": generate, + "miden:c/d": ::foo::Bar + } + }) + .unwrap(); + + assert_eq!(parsed.with_entries.len(), 2); + assert_eq!(parsed.with_entries[0].0, "miden:a/b"); + assert_eq!(parsed.with_entries[1].0, "miden:c/d"); + } + + /// Integration test verifying that `augment_generated_bindings` produces valid Rust code. + /// + /// This test simulates realistic wit-bindgen output with custom types, multiple methods, + /// and verifies the augmented output parses as valid Rust and contains the expected + /// wrapper struct with properly qualified type paths. + #[test] + fn test_augment_generated_bindings_integration() { + // Simulate more realistic wit-bindgen output with types and multiple leaf modules + let src = r#" + mod miden { + mod basic_wallet { + mod basic_wallet { + pub struct AssetInfo { + pub amount: u64, + } + + pub fn receive_asset(asset: AssetInfo) {} + pub fn move_asset_to_note(asset: AssetInfo, note_idx: u32) -> bool { true } + fn _internal_helper() {} // Should be skipped (underscore prefix) + } } - let mut name = token.trim(); - if let Some(idx) = name.find('@') { - name = &name[..idx]; + mod other_component { + mod other_component { + pub fn do_something(value: u64) -> u64 { value } + } } - return Some(name.trim().to_string()); } - } - None - } - - /// Returns the first world identifier from a WIT source string, if present. - pub(super) fn extract_world_name(contents: &str) -> Option { - for line in contents.lines() { - let trimmed = strip_comment(line).trim_start(); - if let Some(rest) = trimmed.strip_prefix("world ") { - let mut name = String::new(); - for ch in rest.trim().chars() { - if ch.is_alphanumeric() || ch == '-' || ch == '_' { - name.push(ch); - } else { - break; + mod exports { + mod my_export { + pub fn exported_fn() {} // Should be skipped (exports module) + } + } + "#; + + let tokens: TokenStream2 = src.parse().unwrap(); + let result = augment_generated_bindings(tokens).unwrap(); + + // Verify the output parses as valid Rust + let parsed: File = + syn::parse2(result.clone()).expect("augmented bindings should be valid Rust syntax"); + + // Find the Account struct and impl + let has_account_struct = parsed + .items + .iter() + .any(|item| matches!(item, Item::Struct(s) if s.ident == "Account")); + let has_account_impl = parsed.items.iter().any(|item| { + matches!(item, Item::Impl(i) if i.self_ty.to_token_stream().to_string() == "Account") + }); + + assert!(has_account_struct, "should generate Account struct"); + assert!(has_account_impl, "should generate Account impl"); + + // Find the impl block and verify methods + let impl_block = parsed + .items + .iter() + .find_map(|item| match item { + Item::Impl(i) if i.self_ty.to_token_stream().to_string() == "Account" => Some(i), + _ => None, + }) + .expect("Account impl should exist"); + + let method_names: Vec = impl_block + .items + .iter() + .filter_map(|item| match item { + ImplItem::Fn(f) => Some(f.sig.ident.to_string()), + _ => None, + }) + .collect(); + + // Should include methods from both leaf modules + assert!(method_names.contains(&"receive_asset".to_string())); + assert!(method_names.contains(&"move_asset_to_note".to_string())); + assert!(method_names.contains(&"do_something".to_string())); + + // Should NOT include internal helper or exported functions + assert!(!method_names.contains(&"_internal_helper".to_string())); + assert!(!method_names.contains(&"exported_fn".to_string())); + + // Verify type qualification in the result string + let result_str = result.to_string(); + // AssetInfo should be qualified with its module path + assert!( + result_str.contains("miden :: basic_wallet :: basic_wallet :: AssetInfo"), + "custom types should be qualified with module path" + ); + } + + #[test] + fn test_method_name_collision_detected() { + // Two different interfaces with the same function name + let src = r#" + mod miden { + mod interface_a { + mod interface_a { + pub fn transfer(amount: u64) {} } } - if !name.is_empty() { - return Some(name); + mod interface_b { + mod interface_b { + pub fn transfer(value: u32) {} + } } } - } - None + "#; + + let tokens: TokenStream2 = src.parse().unwrap(); + let result = augment_generated_bindings(tokens); + + assert!(result.is_err(), "should detect method name collision"); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("method name collision"), + "error should mention collision: {err_msg}" + ); + assert!(err_msg.contains("transfer"), "error should mention the colliding method name"); } - /// Strips line comments starting with `//` from the provided source line. - fn strip_comment(line: &str) -> &str { - match line.split_once("//") { - Some((before, _)) => before, - None => line, - } + #[test] + fn test_no_collision_different_names() { + let src = r#" + mod miden { + mod interface_a { + mod interface_a { + pub fn transfer_a(amount: u64) {} + } + } + mod interface_b { + mod interface_b { + pub fn transfer_b(value: u32) {} + } + } + } + "#; + + let tokens: TokenStream2 = src.parse().unwrap(); + let result = augment_generated_bindings(tokens); + + assert!(result.is_ok(), "should not detect collision for different method names"); } } diff --git a/sdk/base-macros/src/lib.rs b/sdk/base-macros/src/lib.rs index a93e60893..c84b78118 100644 --- a/sdk/base-macros/src/lib.rs +++ b/sdk/base-macros/src/lib.rs @@ -55,6 +55,7 @@ mod boilerplate; mod component_macro; mod export_type; mod generate; +mod manifest_paths; mod script; mod types; mod util; diff --git a/sdk/base-macros/src/manifest_paths.rs b/sdk/base-macros/src/manifest_paths.rs new file mode 100644 index 000000000..9850ff521 --- /dev/null +++ b/sdk/base-macros/src/manifest_paths.rs @@ -0,0 +1,292 @@ +//! Utilities for resolving WIT paths from Cargo.toml manifest metadata. + +use std::{ + env, fs, + io::ErrorKind, + path::{Path, PathBuf}, +}; + +use proc_macro2::Span; +use syn::Error; +use toml::Value; + +use crate::util::{bundled_wit_folder, strip_line_comment}; + +/// File name for the embedded Miden SDK WIT. +const SDK_WIT_FILE_NAME: &str = "miden.wit"; + +/// Embedded Miden SDK WIT source. +pub(crate) const SDK_WIT_SOURCE: &str = include_str!("../wit/miden.wit"); + +/// WIT metadata extracted from the consuming crate. +pub(crate) struct ResolvedWit { + pub paths: Vec, + pub world: Option, +} + +#[derive(Default)] +pub(crate) struct ResolveOptions { + pub allow_missing_local_wit: bool, +} + +/// Collects WIT search paths and the target world from `Cargo.toml` + local files. +pub(crate) fn resolve_wit_paths(options: ResolveOptions) -> Result { + let manifest_dir = env::var("CARGO_MANIFEST_DIR").map_err(|err| { + Error::new(Span::call_site(), format!("failed to read CARGO_MANIFEST_DIR: {err}")) + })?; + let manifest_path = Path::new(&manifest_dir).join("Cargo.toml"); + + let manifest_content = fs::read_to_string(&manifest_path).map_err(|err| { + Error::new( + Span::call_site(), + format!("failed to read manifest '{}': {err}", manifest_path.display()), + ) + })?; + + let manifest: Value = manifest_content.parse().map_err(|err| { + Error::new( + Span::call_site(), + format!("failed to parse manifest '{}': {err}", manifest_path.display()), + ) + })?; + + let canonical_prelude_dir = ensure_sdk_wit()?; + + let mut resolved = Vec::new(); + + let prelude_dir = canonical_prelude_dir + .to_str() + .ok_or_else(|| { + Error::new( + Span::call_site(), + format!("path '{}' contains invalid UTF-8", canonical_prelude_dir.display()), + ) + })? + .to_owned(); + + resolved.push(prelude_dir); + + if let Some(dependencies) = manifest + .get("package") + .and_then(Value::as_table) + .and_then(|package| package.get("metadata")) + .and_then(Value::as_table) + .and_then(|metadata| metadata.get("component")) + .and_then(Value::as_table) + .and_then(|component| component.get("target")) + .and_then(Value::as_table) + .and_then(|target| target.get("dependencies")) + .and_then(Value::as_table) + { + for (name, dependency) in dependencies.iter() { + let table = dependency.as_table().ok_or_else(|| { + Error::new( + Span::call_site(), + format!( + "dependency '{name}' under \ + [package.metadata.component.target.dependencies] must be a table" + ), + ) + })?; + + let path_value = table.get("path").and_then(Value::as_str).ok_or_else(|| { + Error::new( + Span::call_site(), + format!("dependency '{name}' is missing a 'path' entry"), + ) + })?; + + let raw_path = PathBuf::from(path_value); + let absolute = if raw_path.is_absolute() { + raw_path + } else { + Path::new(&manifest_dir).join(&raw_path) + }; + + let canonical = fs::canonicalize(&absolute).unwrap_or_else(|_| absolute.clone()); + + let metadata = fs::metadata(&canonical).map_err(|err| { + Error::new( + Span::call_site(), + format!( + "failed to read metadata for dependency '{name}' path '{}': {err}", + canonical.display() + ), + ) + })?; + + let search_path = if metadata.is_dir() { + canonical + } else if let Some(parent) = canonical.parent() { + parent.to_path_buf() + } else { + return Err(Error::new( + Span::call_site(), + format!( + "dependency '{name}' path '{}' does not have a parent directory", + canonical.display() + ), + )); + }; + + let path_str = search_path.to_str().ok_or_else(|| { + Error::new( + Span::call_site(), + format!("dependency '{name}' path contains invalid UTF-8"), + ) + })?; + + if !resolved.iter().any(|existing| existing == path_str) { + resolved.push(path_str.to_owned()); + } + } + } + + let local_wit_root = Path::new(&manifest_dir).join("wit"); + let mut world = None; + + if local_wit_root.exists() && !options.allow_missing_local_wit { + let local_root = fs::canonicalize(&local_wit_root).unwrap_or(local_wit_root); + let local_root_str = local_root.to_str().ok_or_else(|| { + Error::new( + Span::call_site(), + format!("path '{}' contains invalid UTF-8", local_root.display()), + ) + })?; + if !resolved.iter().any(|existing| existing == local_root_str) { + resolved.push(local_root_str.to_owned()); + } + world = detect_world_name(&local_root)?; + } + + Ok(ResolvedWit { + paths: resolved, + world, + }) +} + +/// Ensures the embedded Miden SDK WIT is materialized in the project's folder. +fn ensure_sdk_wit() -> Result { + let autogenerated_wit_folder = bundled_wit_folder()?; + + let sdk_wit_path = autogenerated_wit_folder.join(SDK_WIT_FILE_NAME); + let sdk_version: &str = env!("CARGO_PKG_VERSION"); + let expected_source = format!( + "/// NOTE: This file is auto-generated from the Miden SDK.\n/// Version: \ + v{sdk_version}\n/// Any manual edits will be overwritten.\n\n{SDK_WIT_SOURCE}" + ); + let should_write_wit = match fs::read_to_string(&sdk_wit_path) { + Ok(existing) => existing != expected_source, + Err(err) if err.kind() == ErrorKind::NotFound => true, + Err(err) => { + return Err(Error::new( + Span::call_site(), + format!("failed to read '{}': {err}", sdk_wit_path.display()), + )); + } + }; + + if should_write_wit { + fs::write(&sdk_wit_path, expected_source).map_err(|err| { + Error::new( + Span::call_site(), + format!("failed to write '{}': {err}", sdk_wit_path.display()), + ) + })?; + } + + Ok(fs::canonicalize(&autogenerated_wit_folder).unwrap_or(autogenerated_wit_folder)) +} + +/// Scans the component's `wit` directory to find the default world. +fn detect_world_name(wit_root: &Path) -> Result, Error> { + let mut entries = fs::read_dir(wit_root) + .map_err(|err| { + Error::new(Span::call_site(), format!("failed to read '{}': {err}", wit_root.display())) + })? + .collect::, _>>() + .map_err(|err| { + Error::new( + Span::call_site(), + format!("failed to iterate '{}': {err}", wit_root.display()), + ) + })?; + entries.sort_by_key(|entry| entry.file_name()); + + for entry in entries { + let path = entry.path(); + if path.file_name().is_some_and(|name| name == "deps") { + continue; + } + if path.is_dir() { + continue; + } + if path.extension().and_then(|ext| ext.to_str()) != Some("wit") { + continue; + } + + if let Some((package, world)) = parse_package_and_world(&path)? { + return Ok(Some(format!("{package}/{world}"))); + } + } + + Ok(None) +} + +/// Parses a WIT source file for its package declaration and first world definition. +fn parse_package_and_world(path: &Path) -> Result, Error> { + let contents = fs::read_to_string(path).map_err(|err| { + Error::new( + Span::call_site(), + format!("failed to read WIT file '{}': {err}", path.display()), + ) + })?; + + let package = extract_package_name(&contents); + let world = extract_world_name(&contents); + + match (package, world) { + (Some(package), Some(world)) => Ok(Some((package, world))), + _ => Ok(None), + } +} + +/// Returns the package identifier from a WIT source string, if present. +fn extract_package_name(contents: &str) -> Option { + for line in contents.lines() { + let trimmed = strip_line_comment(line).trim_start(); + if let Some(rest) = trimmed.strip_prefix("package ") { + let mut token = rest.trim(); + if let Some(idx) = token.find(';') { + token = &token[..idx]; + } + let mut name = token.trim(); + if let Some(idx) = name.find('@') { + name = &name[..idx]; + } + return Some(name.trim().to_string()); + } + } + None +} + +/// Extracts the first world identifier from a WIT source string. +pub(crate) fn extract_world_name(contents: &str) -> Option { + for line in contents.lines() { + let trimmed = strip_line_comment(line).trim_start(); + if let Some(rest) = trimmed.strip_prefix("world ") { + let mut name = String::new(); + for ch in rest.trim().chars() { + if ch.is_alphanumeric() || ch == '-' || ch == '_' { + name.push(ch); + } else { + break; + } + } + if !name.is_empty() { + return Some(name); + } + } + } + None +} diff --git a/sdk/base-macros/src/script.rs b/sdk/base-macros/src/script.rs index 8d82a080c..6d4b14b9f 100644 --- a/sdk/base-macros/src/script.rs +++ b/sdk/base-macros/src/script.rs @@ -5,7 +5,10 @@ use quote::quote; use syn::{parse_macro_input, spanned::Spanned, FnArg, ItemFn, Pat, PatIdent}; use toml::Value; -use crate::{boilerplate::runtime_boilerplate, util::generated_wit_folder_at}; +use crate::{ + boilerplate::runtime_boilerplate, + util::{generated_wit_folder_at, strip_line_comment}, +}; const SCRIPT_PACKAGE_VERSION: &str = "1.0.0"; @@ -44,27 +47,13 @@ pub(crate) fn expand( .into(); } - let mut call_args: Vec = Vec::new(); - for arg in &input_fn.sig.inputs { - match arg { - FnArg::Typed(pat_type) => match pat_type.pat.as_ref() { - Pat::Ident(PatIdent { ident, .. }) => call_args.push(ident.clone()), - other => { - return syn::Error::new( - other.span(), - "function arguments must be simple identifiers", - ) - .into_compile_error() - .into(); - } - }, - FnArg::Receiver(receiver) => { - return syn::Error::new(receiver.span(), "unexpected receiver argument") - .into_compile_error() - .into(); - } - } - } + // Parse the optional second parameter (injected wrapper struct). + // The trait requires `fn run(arg: Word)`, so if the user declares a second parameter, + // it will be instantiated via `Default::default()` and passed to the user's function. + let injected_param = match parse_injected_param(&input_fn) { + Ok(param) => param, + Err(err) => return err.into_compile_error().into(), + }; let inline_wit = match build_script_wit(Span::call_site(), config.export_interface) { Ok(wit) => wit, @@ -73,15 +62,6 @@ pub(crate) fn expand( let inline_literal = Literal::string(&inline_wit); let struct_ident = quote::format_ident!("Struct"); - let fn_inputs = &input_fn.sig.inputs; - let fn_output = &input_fn.sig.output; - - let call = if call_args.is_empty() { - quote! { #fn_ident() } - } else { - quote! { #fn_ident(#(#call_args),*) } - }; - let export_path: syn::Path = match syn::parse_str(config.guest_trait_path) { Ok(path) => path, Err(err) => { @@ -96,6 +76,15 @@ pub(crate) fn expand( let runtime_boilerplate = runtime_boilerplate(); + // Generate the call to the user's function, with optional injected parameter + let (instantiation, call) = match &injected_param { + Some((ident, ty)) => ( + quote! { let #ident = <#ty as ::core::default::Default>::default(); }, + quote! { #fn_ident(arg, #ident) }, + ), + None => (quote! {}, quote! { #fn_ident(arg) }), + }; + let expanded = quote! { #runtime_boilerplate @@ -108,7 +97,8 @@ pub(crate) fn expand( pub struct #struct_ident; impl #export_path for #struct_ident { - fn run(#fn_inputs) #fn_output { + fn run(arg: ::miden::Word) { + #instantiation #call; } } @@ -117,6 +107,88 @@ pub(crate) fn expand( expanded.into() } +/// Parses the optional injected parameter from the user's `fn run` signature. +/// +/// The trait requires `fn run(arg: Word)`. If the user declares a second parameter, +/// it is treated as an "injected" wrapper struct that will be instantiated via +/// `Default::default()` and passed to the user's function. +/// +/// Only up to 2 parameters are supported: `(arg: Word)` or `(arg: Word, account: Account)`. +/// +/// Returns `Some((ident, type))` if a second parameter exists, `None` otherwise. +fn parse_injected_param(input_fn: &ItemFn) -> syn::Result> { + if input_fn.sig.inputs.is_empty() { + return Err(syn::Error::new( + input_fn.sig.span(), + "fn run requires at least one parameter: (arg: Word) or (arg: Word, account: Account)", + )); + } + + if input_fn.sig.inputs.len() > 2 { + return Err(syn::Error::new( + input_fn.sig.span(), + "fn run accepts at most 2 parameters: (arg: Word) or (arg: Word, account: Account)", + )); + } + + // Validate the first parameter is `arg: Word` + let first_arg = input_fn.sig.inputs.first().unwrap(); + match first_arg { + FnArg::Typed(pat_type) => { + if !matches!(pat_type.pat.as_ref(), Pat::Ident(_)) { + return Err(syn::Error::new( + pat_type.pat.span(), + "first parameter must be a simple identifier (e.g., `arg: Word`)", + )); + } + // Check that the type is `Word` + if !is_word_type(&pat_type.ty) { + return Err(syn::Error::new( + pat_type.ty.span(), + "first parameter must have type `Word` (e.g., `arg: Word`)", + )); + } + } + FnArg::Receiver(receiver) => { + return Err(syn::Error::new(receiver.span(), "unexpected receiver argument")); + } + } + + let Some(second_arg) = input_fn.sig.inputs.iter().nth(1) else { + return Ok(None); + }; + + match second_arg { + FnArg::Typed(pat_type) => { + let ident = match pat_type.pat.as_ref() { + Pat::Ident(PatIdent { ident, .. }) => ident.clone(), + other => { + return Err(syn::Error::new( + other.span(), + "function arguments must be simple identifiers", + )); + } + }; + Ok(Some((ident, (*pat_type.ty).clone()))) + } + FnArg::Receiver(receiver) => { + Err(syn::Error::new(receiver.span(), "unexpected receiver argument")) + } + } +} + +/// Checks if a type is `Word` (handles both `Word` and `miden::Word` paths). +fn is_word_type(ty: &syn::Type) -> bool { + let syn::Type::Path(type_path) = ty else { + return false; + }; + if type_path.qself.is_some() { + return false; + } + let last_segment = type_path.path.segments.last(); + last_segment.is_some_and(|seg| seg.ident == "Word" && seg.arguments.is_empty()) +} + fn build_script_wit( error_span: Span, export_interface: &'static str, @@ -341,7 +413,7 @@ fn parse_wit_file(path: &Path) -> Result, String> { fn extract_package_identifier(contents: &str) -> Option<(String, Option)> { for line in contents.lines() { - let trimmed = strip_comment(line).trim_start(); + let trimmed = strip_line_comment(line).trim_start(); if let Some(rest) = trimmed.strip_prefix("package ") { let token = rest.trim_end_matches(';').trim(); if let Some((name, version)) = token.split_once('@') { @@ -357,7 +429,7 @@ fn extract_world_exports(contents: &str) -> Vec { let mut exports = Vec::new(); for line in contents.lines() { - let trimmed = strip_comment(line).trim(); + let trimmed = strip_line_comment(line).trim(); if let Some(rest) = trimmed.strip_prefix("export ") { let rest = rest.trim_end_matches(';').trim(); let interface = match rest.split_once(':') { @@ -372,10 +444,3 @@ fn extract_world_exports(contents: &str) -> Vec { exports } - -fn strip_comment(line: &str) -> &str { - match line.split_once("//") { - Some((before, _)) => before, - None => line, - } -} diff --git a/sdk/base-macros/src/types.rs b/sdk/base-macros/src/types.rs index ea01d012f..9951c56cf 100644 --- a/sdk/base-macros/src/types.rs +++ b/sdk/base-macros/src/types.rs @@ -9,7 +9,7 @@ use heck::ToKebabCase; use proc_macro2::Span; use syn::{spanned::Spanned, ItemStruct, Type}; -use crate::generate::SDK_WIT_SOURCE; +use crate::manifest_paths::SDK_WIT_SOURCE; #[derive(Clone, Debug)] pub(crate) struct TypeRef { diff --git a/sdk/base-macros/src/util.rs b/sdk/base-macros/src/util.rs index 0d90f2fbf..a4d43bab0 100644 --- a/sdk/base-macros/src/util.rs +++ b/sdk/base-macros/src/util.rs @@ -59,3 +59,16 @@ pub fn generated_wit_folder_at(manifest_dir: &Path) -> Result { })?; Ok(wit_deps_dir) } + +/// Strips line comments starting with `//` from the provided source line. +/// +/// Returns the portion of the line before the comment, or the entire line if no comment exists. +/// +/// **Note:** This is a simple heuristic that doesn't account for `//` appearing +/// inside string literals. Only use for WIT source parsing where this is not an issue. +pub fn strip_line_comment(line: &str) -> &str { + match line.split_once("//") { + Some((before, _)) => before, + None => line, + } +} diff --git a/tests/rust-apps-wasm/rust-sdk/add/Cargo.lock b/tests/rust-apps-wasm/rust-sdk/add/Cargo.lock index c221a348a..5891e0a4a 100644 --- a/tests/rust-apps-wasm/rust-sdk/add/Cargo.lock +++ b/tests/rust-apps-wasm/rust-sdk/add/Cargo.lock @@ -866,6 +866,8 @@ dependencies = [ "semver 1.0.26", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/tests/rust-apps-wasm/rust-sdk/component-macros-account/Cargo.lock b/tests/rust-apps-wasm/rust-sdk/component-macros-account/Cargo.lock index 3707ae2a0..7dc8f3d9e 100644 --- a/tests/rust-apps-wasm/rust-sdk/component-macros-account/Cargo.lock +++ b/tests/rust-apps-wasm/rust-sdk/component-macros-account/Cargo.lock @@ -886,6 +886,8 @@ dependencies = [ "semver 1.0.27", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/tests/rust-apps-wasm/rust-sdk/component-macros-note/Cargo.lock b/tests/rust-apps-wasm/rust-sdk/component-macros-note/Cargo.lock index 863bc24c8..a81a3cfa8 100644 --- a/tests/rust-apps-wasm/rust-sdk/component-macros-note/Cargo.lock +++ b/tests/rust-apps-wasm/rust-sdk/component-macros-note/Cargo.lock @@ -886,6 +886,8 @@ dependencies = [ "semver 1.0.27", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/tests/rust-apps-wasm/rust-sdk/cross-ctx-account-word-arg/Cargo.lock b/tests/rust-apps-wasm/rust-sdk/cross-ctx-account-word-arg/Cargo.lock index e5e8eab32..22ceac4c7 100644 --- a/tests/rust-apps-wasm/rust-sdk/cross-ctx-account-word-arg/Cargo.lock +++ b/tests/rust-apps-wasm/rust-sdk/cross-ctx-account-word-arg/Cargo.lock @@ -873,6 +873,8 @@ dependencies = [ "semver 1.0.26", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/tests/rust-apps-wasm/rust-sdk/cross-ctx-account-word/Cargo.lock b/tests/rust-apps-wasm/rust-sdk/cross-ctx-account-word/Cargo.lock index 5ea05f47b..8a552f1c8 100644 --- a/tests/rust-apps-wasm/rust-sdk/cross-ctx-account-word/Cargo.lock +++ b/tests/rust-apps-wasm/rust-sdk/cross-ctx-account-word/Cargo.lock @@ -873,6 +873,8 @@ dependencies = [ "semver 1.0.26", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/tests/rust-apps-wasm/rust-sdk/cross-ctx-account/Cargo.lock b/tests/rust-apps-wasm/rust-sdk/cross-ctx-account/Cargo.lock index 1881f4ec9..430ae8693 100644 --- a/tests/rust-apps-wasm/rust-sdk/cross-ctx-account/Cargo.lock +++ b/tests/rust-apps-wasm/rust-sdk/cross-ctx-account/Cargo.lock @@ -873,6 +873,8 @@ dependencies = [ "semver 1.0.26", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/tests/rust-apps-wasm/rust-sdk/cross-ctx-note-word-arg/Cargo.lock b/tests/rust-apps-wasm/rust-sdk/cross-ctx-note-word-arg/Cargo.lock index 8a557f7fe..2a5e8e74b 100644 --- a/tests/rust-apps-wasm/rust-sdk/cross-ctx-note-word-arg/Cargo.lock +++ b/tests/rust-apps-wasm/rust-sdk/cross-ctx-note-word-arg/Cargo.lock @@ -873,6 +873,8 @@ dependencies = [ "semver 1.0.26", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/tests/rust-apps-wasm/rust-sdk/cross-ctx-note-word/Cargo.lock b/tests/rust-apps-wasm/rust-sdk/cross-ctx-note-word/Cargo.lock index 96af9c888..2374b4622 100644 --- a/tests/rust-apps-wasm/rust-sdk/cross-ctx-note-word/Cargo.lock +++ b/tests/rust-apps-wasm/rust-sdk/cross-ctx-note-word/Cargo.lock @@ -873,6 +873,8 @@ dependencies = [ "semver 1.0.26", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]] diff --git a/tests/rust-apps-wasm/rust-sdk/cross-ctx-note/Cargo.lock b/tests/rust-apps-wasm/rust-sdk/cross-ctx-note/Cargo.lock index 4a4be16ab..bd187420d 100644 --- a/tests/rust-apps-wasm/rust-sdk/cross-ctx-note/Cargo.lock +++ b/tests/rust-apps-wasm/rust-sdk/cross-ctx-note/Cargo.lock @@ -873,6 +873,8 @@ dependencies = [ "semver 1.0.26", "syn", "toml 0.8.23", + "wit-bindgen-core", + "wit-bindgen-rust", ] [[package]]