1use proc_macro::TokenStream;
4use proc_macro_crate::{FoundCrate, crate_name};
5use quote::{format_ident, quote};
6use std::path::{Path, PathBuf};
7use syn::{Attribute, DeriveInput, Ident, ImplItem, Item, ItemImpl, ItemStruct, Type, TypePath};
8use wit_parser::{FunctionKind, Resolve, TypeDefKind, WorldItem};
9
10#[proc_macro_attribute]
12pub fn skip(_attr: TokenStream, item: TokenStream) -> TokenStream {
13 item
14}
15
16#[proc_macro_derive(WasvyComponent)]
21pub fn derive_wasvy_component(input: TokenStream) -> TokenStream {
22 let input = syn::parse_macro_input!(input as DeriveInput);
23 let wasvy_path = wasvy_path();
24 let ident = &input.ident;
25 let register_ident = format_ident!("__wasvy_register_component_{}", ident);
26
27 let expanded = quote! {
28 impl #wasvy_path::authoring::WasvyComponent for #ident {}
29
30 #[allow(non_snake_case)]
31 fn #register_ident(app: &mut #wasvy_path::authoring::App) {
32 <#ident as #wasvy_path::authoring::WasvyComponent>::register(app);
33 }
34
35 #wasvy_path::__wasvy_submit_component_registration!(
36 #wasvy_path::authoring::WasvyComponentRegistration { register: #register_ident }
37 );
38 };
39
40 expanded.into()
41}
42
43#[proc_macro]
57pub fn auto_host_components(input: TokenStream) -> TokenStream {
58 let args = syn::parse_macro_input!(input as AutoHostArgs);
59 match expand_auto_host_components(args) {
60 Ok(tokens) => tokens.into(),
61 Err(err) => err.to_compile_error().into(),
62 }
63}
64
65#[proc_macro]
80pub fn guest_type_paths(input: TokenStream) -> TokenStream {
81 let args = syn::parse_macro_input!(input as GuestTypePathsArgs);
82 match expand_guest_type_paths(args) {
83 Ok(tokens) => tokens.into(),
84 Err(err) => err.to_compile_error().into(),
85 }
86}
87
88#[proc_macro]
100pub fn guest_bindings(input: TokenStream) -> TokenStream {
101 let input_tokens = proc_macro2::TokenStream::from(input.clone());
102 let args = syn::parse_macro_input!(input as GuestBindingsArgs);
103 match expand_guest_bindings(args, input_tokens) {
104 Ok(tokens) => tokens.into(),
105 Err(err) => err.to_compile_error().into(),
106 }
107}
108
109#[proc_macro]
121pub fn include_wasvy_components(input: TokenStream) -> TokenStream {
122 let args = syn::parse_macro_input!(input as IncludeComponentsArgs);
123 match expand_include_components(args) {
124 Ok(tokens) => tokens.into(),
125 Err(err) => err.to_compile_error().into(),
126 }
127}
128
129#[deprecated(note = "Use #[derive(WasvyComponent)] instead")]
143#[proc_macro_attribute]
144pub fn component(_attr: TokenStream, item: TokenStream) -> TokenStream {
145 let input = syn::parse_macro_input!(item as Item);
146 let wasvy_path = wasvy_path();
147
148 let expanded = match input {
149 Item::Struct(item) => expand_component_struct(item, &wasvy_path),
150 Item::Enum(item) => {
151 let ident = &item.ident;
152 let register_ident = format_ident!("__wasvy_register_component_{}", ident);
153 quote! {
154 #item
155
156 impl #wasvy_path::authoring::WasvyComponent for #ident {}
157
158 #[allow(non_snake_case)]
159 fn #register_ident(app: &mut #wasvy_path::authoring::App) {
160 <#ident as #wasvy_path::authoring::WasvyComponent>::register(app);
161 }
162
163 #wasvy_path::__wasvy_submit_component_registration!(
164 #wasvy_path::authoring::WasvyComponentRegistration { register: #register_ident }
165 );
166 }
167 }
168 other => {
169 return syn::Error::new_spanned(
170 other,
171 "#[wasvy::component] can only be applied to structs or enums",
172 )
173 .to_compile_error()
174 .into();
175 }
176 };
177
178 expanded.into()
179}
180
181#[proc_macro_attribute]
197pub fn methods(_attr: TokenStream, item: TokenStream) -> TokenStream {
198 let input = syn::parse_macro_input!(item as ItemImpl);
199 let wasvy_path = wasvy_path();
200
201 let Some(self_ty) = extract_type_path(&input.self_ty) else {
202 return syn::Error::new_spanned(
203 input.self_ty,
204 "#[wasvy::methods] requires a concrete type (no generics)",
205 )
206 .to_compile_error()
207 .into();
208 };
209
210 let type_ident = self_ty
211 .path
212 .segments
213 .last()
214 .map(|seg| seg.ident.clone())
215 .unwrap();
216 let mut method_idents = Vec::new();
217 let mut items = Vec::new();
218 let mut metadata_submits = Vec::new();
219 let mut errors: Option<syn::Error> = None;
220
221 for item in input.items.into_iter() {
222 match item {
223 ImplItem::Fn(mut func) => {
224 let skip = has_wasvy_skip_attr(&func.attrs);
225 func.attrs.retain(|attr| !is_wasvy_skip_attr(attr));
226
227 if skip {
228 items.push(ImplItem::Fn(func));
229 continue;
230 }
231
232 match func.sig.receiver() {
233 Some(receiver) => {
234 if receiver.reference.is_none() {
235 let err = syn::Error::new_spanned(
236 &func.sig,
237 "#[wasvy::methods] only supports &self or &mut self receivers; add #[wasvy::skip] or move this method to another impl",
238 );
239 if let Some(errors) = errors.as_mut() {
240 errors.combine(err);
241 } else {
242 errors = Some(err);
243 }
244 }
245 }
246 None => {
247 let err = syn::Error::new_spanned(
248 &func.sig,
249 "#[wasvy::methods] requires a self receiver; add #[wasvy::skip] or move this method to another impl",
250 );
251 if let Some(errors) = errors.as_mut() {
252 errors.combine(err);
253 } else {
254 errors = Some(err);
255 }
256 }
257 }
258
259 if !func.sig.generics.params.is_empty() {
260 let err = syn::Error::new_spanned(
261 &func.sig.generics,
262 "#[wasvy::methods] does not support generic methods; add #[wasvy::skip] or move this method to another impl",
263 );
264 if let Some(errors) = errors.as_mut() {
265 errors.combine(err);
266 } else {
267 errors = Some(err);
268 }
269 }
270
271 let method_ident = func.sig.ident.clone();
272 let type_path_expr = quote!(concat!(module_path!(), "::", stringify!(#type_ident)));
273 let method_lit =
274 syn::LitStr::new(&method_ident.to_string(), proc_macro2::Span::call_site());
275 let mut arg_names = Vec::new();
276 for (idx, arg) in func.sig.inputs.iter().skip(1).enumerate() {
277 if let syn::FnArg::Typed(pat) = arg {
278 match pat.pat.as_ref() {
279 syn::Pat::Ident(ident) => {
280 arg_names.push(ident.ident.to_string());
281 }
282 _ => {
283 let err = syn::Error::new_spanned(
284 pat.pat.as_ref(),
285 format!(
286 "#[wasvy::methods] only supports identifier arguments; add #[wasvy::skip] or rename parameter {} to an identifier",
287 idx
288 ),
289 );
290 if let Some(errors) = errors.as_mut() {
291 errors.combine(err);
292 } else {
293 errors = Some(err);
294 }
295 }
296 }
297 }
298 }
299 let arg_name_lits: Vec<syn::LitStr> = arg_names
300 .iter()
301 .map(|name| syn::LitStr::new(name, proc_macro2::Span::call_site()))
302 .collect();
303 let metadata_ident = format_ident!("__wasvy_args_{}_{}", type_ident, method_ident);
304
305 metadata_submits.push(quote! {
306 #[allow(non_upper_case_globals)]
307 const #metadata_ident: &[&str] = &[#(#arg_name_lits),*];
308 #wasvy_path::__wasvy_submit_method_metadata!(
309 #wasvy_path::authoring::WasvyMethodMetadata {
310 type_path: #type_path_expr,
311 method: #method_lit,
312 arg_names: #metadata_ident,
313 }
314 );
315 });
316
317 method_idents.push(method_ident);
318 items.push(ImplItem::Fn(func));
319 }
320 other => items.push(other),
321 }
322 }
323
324 let impl_block = ItemImpl { items, ..input };
325
326 let register_ident = format_ident!("__wasvy_register_methods_{}", type_ident);
327
328 let expanded = quote! {
329 #impl_block
330
331 impl #wasvy_path::authoring::WasvyMethods for #type_ident {
332 fn register_methods(app: &mut #wasvy_path::authoring::App) {
333 #(
334 app.register_function(#type_ident::#method_idents);
335 )*
336 }
337 }
338
339 #[allow(non_snake_case)]
340 fn #register_ident(app: &mut #wasvy_path::authoring::App) {
341 <#type_ident as #wasvy_path::authoring::WasvyMethods>::register_methods(app);
342 }
343
344 #(#metadata_submits)*
345
346 #wasvy_path::__wasvy_submit_methods_registration!(
347 #wasvy_path::authoring::WasvyMethodsRegistration { register: #register_ident }
348 );
349 };
350
351 if let Some(err) = errors {
352 err.to_compile_error().into()
353 } else {
354 expanded.into()
355 }
356}
357
358fn expand_component_struct(
359 item: ItemStruct,
360 wasvy_path: &proc_macro2::TokenStream,
361) -> proc_macro2::TokenStream {
362 let ident = &item.ident;
363 let register_ident = format_ident!("__wasvy_register_component_{}", ident);
364 quote! {
365 #item
366
367 impl #wasvy_path::authoring::WasvyComponent for #ident {}
368
369 #[allow(non_snake_case)]
370 fn #register_ident(app: &mut #wasvy_path::authoring::App) {
371 <#ident as #wasvy_path::authoring::WasvyComponent>::register(app);
372 }
373
374 #wasvy_path::__wasvy_submit_component_registration!(
375 #wasvy_path::authoring::WasvyComponentRegistration { register: #register_ident }
376 );
377 }
378}
379
380fn has_wasvy_skip_attr(attrs: &[Attribute]) -> bool {
381 attrs.iter().any(is_wasvy_skip_attr)
382}
383
384fn is_wasvy_skip_attr(attr: &Attribute) -> bool {
385 attr.path()
386 .segments
387 .last()
388 .is_some_and(|seg| seg.ident == "skip")
389}
390
391fn extract_type_path(ty: &Type) -> Option<&TypePath> {
392 match ty {
393 Type::Path(path) if path.qself.is_none() => Some(path),
394 _ => None,
395 }
396}
397
398fn wasvy_path() -> proc_macro2::TokenStream {
399 let name = match crate_name("wasvy") {
400 Ok(FoundCrate::Name(name)) => name,
401 Ok(FoundCrate::Itself) | Err(_) => "wasvy".to_string(),
402 };
403 let ident = Ident::new(&name, proc_macro2::Span::call_site());
404 quote!(::#ident)
405}
406
407struct AutoHostArgs {
408 path: syn::LitStr,
409 world: syn::LitStr,
410 module: Ident,
411}
412
413struct GuestTypePathsArgs {
414 path: syn::LitStr,
415 package: syn::LitStr,
416 interface: syn::LitStr,
417 module: syn::Path,
418}
419
420struct GuestBindingsArgs {
421 paths: Vec<syn::LitStr>,
422}
423
424struct IncludeComponentsArgs {
425 path: syn::LitStr,
426}
427
428impl syn::parse::Parse for AutoHostArgs {
429 fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result<Self> {
430 let mut path = None;
431 let mut world = None;
432 let mut module = None;
433
434 while !input.is_empty() {
435 let key: Ident = input.parse()?;
436 let _: syn::Token![=] = input.parse()?;
437 match key.to_string().as_str() {
438 "path" => path = Some(input.parse()?),
439 "world" => world = Some(input.parse()?),
440 "module" => module = Some(input.parse()?),
441 other => {
442 return Err(syn::Error::new(
443 key.span(),
444 format!("unknown key `{other}`"),
445 ));
446 }
447 }
448
449 if input.peek(syn::Token![,]) {
450 let _: syn::Token![,] = input.parse()?;
451 }
452 }
453
454 Ok(Self {
455 path: path.ok_or_else(|| input.error("missing `path`"))?,
456 world: world.ok_or_else(|| input.error("missing `world`"))?,
457 module: module.unwrap_or_else(|| {
458 Ident::new("components_bindings", proc_macro2::Span::call_site())
459 }),
460 })
461 }
462}
463
464impl syn::parse::Parse for GuestTypePathsArgs {
465 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
466 let mut path = None;
467 let mut package = None;
468 let mut interface = None;
469 let mut module = None;
470
471 while !input.is_empty() {
472 let key: Ident = input.parse()?;
473 let _: syn::Token![=] = input.parse()?;
474 match key.to_string().as_str() {
475 "path" => path = Some(input.parse()?),
476 "package" => package = Some(input.parse()?),
477 "interface" => interface = Some(input.parse()?),
478 "module" => module = Some(input.parse()?),
479 _ => return Err(input.error("unsupported key")),
480 }
481
482 if input.peek(syn::Token![,]) {
483 let _: syn::Token![,] = input.parse()?;
484 }
485 }
486
487 Ok(Self {
488 path: path.ok_or_else(|| input.error("missing `path`"))?,
489 package: package.ok_or_else(|| input.error("missing `package`"))?,
490 interface: interface.ok_or_else(|| input.error("missing `interface`"))?,
491 module: module.ok_or_else(|| input.error("missing `module`"))?,
492 })
493 }
494}
495
496impl syn::parse::Parse for GuestBindingsArgs {
497 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
498 let tokens: proc_macro2::TokenStream = input.parse()?;
499 let paths = extract_paths_from_stream(tokens)?;
500 Ok(Self { paths })
501 }
502}
503
504impl syn::parse::Parse for IncludeComponentsArgs {
505 fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
506 let lit: syn::LitStr = input.parse()?;
507 Ok(Self { path: lit })
508 }
509}
510
511fn expand_auto_host_components(args: AutoHostArgs) -> syn::Result<proc_macro2::TokenStream> {
512 let wasvy_path = wasvy_path();
513 let path_value = resolve_wit_path_with_fallbacks(&args.path);
514 let world_value = args.world.value();
515
516 let mut resolve = Resolve::default();
517 let (pkg_id, _sources) = resolve
518 .push_path(&path_value)
519 .map_err(|err| syn::Error::new(args.path.span(), err.to_string()))?;
520 let world_id = resolve
521 .select_world(&[pkg_id], Some(&world_value))
522 .map_err(|err| syn::Error::new(args.world.span(), err.to_string()))?;
523 let world = &resolve.worlds[world_id];
524
525 let interface_id = world
526 .imports
527 .iter()
528 .find_map(|(name, item)| match (name, item) {
529 (wit_parser::WorldKey::Name(name), WorldItem::Interface { id, .. })
530 if name == "components" =>
531 {
532 Some(*id)
533 }
534 (wit_parser::WorldKey::Interface(id), WorldItem::Interface { .. }) => {
535 let iface = &resolve.interfaces[*id];
536 if iface.name.as_deref() == Some("components") {
537 Some(*id)
538 } else {
539 None
540 }
541 }
542 _ => None,
543 })
544 .ok_or_else(|| {
545 syn::Error::new(args.world.span(), "missing `components` interface import")
546 })?;
547 let interface = &resolve.interfaces[interface_id];
548 let package_id = interface
549 .package
550 .ok_or_else(|| syn::Error::new(args.world.span(), "interface has no package"))?;
551 let package = &resolve.packages[package_id];
552
553 let pkg_namespace = rust_ident(&package.name.namespace.to_string());
554 let pkg_name = rust_ident(&package.name.name.to_string());
555 let interface_name = rust_ident(interface.name.as_deref().unwrap_or("components"));
556
557 let module_ident = args.module;
558
559 let mut with_entries = Vec::new();
560 for (name, type_id) in interface.types.iter() {
561 let type_def = &resolve.types[*type_id];
562 if !matches!(type_def.kind, TypeDefKind::Resource) {
563 continue;
564 }
565 let path = format!(
566 "{}:{}/{}.{}",
567 package.name.namespace,
568 package.name.name,
569 interface.name.as_deref().unwrap_or("components"),
570 name
571 );
572 let lit = syn::LitStr::new(&path, proc_macro2::Span::call_site());
573 with_entries.push(quote!(#lit: #wasvy_path::host::WasmComponent));
574 }
575
576 let wasvy_component =
577 syn::LitStr::new("wasvy:ecs/app.component", proc_macro2::Span::call_site());
578 with_entries.push(quote!(#wasvy_component: #wasvy_path::host::WasmComponent));
579
580 let mut impls = Vec::new();
581
582 for (name, type_id) in interface.types.iter() {
583 let type_def = &resolve.types[*type_id];
584 if !matches!(type_def.kind, TypeDefKind::Resource) {
585 continue;
586 }
587 let trait_ident = format_ident!("Host{}", upper_camel(name));
588
589 let mut methods = Vec::new();
590 for function in interface.functions.values() {
591 match function.kind {
592 FunctionKind::Constructor(id) if id == *type_id => {
593 let params = render_params(&resolve, &function.params, &wasvy_path, true);
594 let ret_tokens =
595 quote!(::wasmtime::component::Resource<#wasvy_path::host::WasmComponent>);
596 let body = quote!(component);
597 methods.push(quote! {
598 fn new(&mut self, #params) -> #ret_tokens {
599 #body
600 }
601 });
602 }
603 FunctionKind::Method(id) if id == *type_id => {
604 let method_name = method_name(&function.name);
605 let method_ident = rust_ident(&method_name);
606 let params = render_params(&resolve, &function.params, &wasvy_path, false);
607 let ret = render_return(&resolve, function.result.as_ref(), &wasvy_path);
608 let invoke = render_invoke_body(
609 &method_name,
610 &function.params,
611 function.result.as_ref(),
612 &wasvy_path,
613 );
614 methods.push(quote! {
615 fn #method_ident(&mut self, #params) #ret {
616 #invoke
617 }
618 });
619 }
620 _ => {}
621 }
622 }
623
624 methods.push(quote! {
625 fn drop(&mut self, component: ::wasmtime::component::Resource<#wasvy_path::host::WasmComponent>) -> Result<(), ::wasmtime::Error> {
626 let _ = component;
627 Ok(())
628 }
629 });
630
631 let trait_path =
632 quote!(#module_ident::#pkg_namespace::#pkg_name::#interface_name::#trait_ident);
633 impls.push(quote! {
634 impl #trait_path for #wasvy_path::host::WasmHost {
635 #(#methods)*
636 }
637 });
638 }
639
640 let trait_host_path = quote!(#module_ident::#pkg_namespace::#pkg_name::#interface_name::Host);
641 let add_to_linker_path =
642 quote!(#module_ident::#pkg_namespace::#pkg_name::#interface_name::add_to_linker);
643
644 let expanded = quote! {
645 mod #module_ident {
646 ::wasmtime::component::bindgen!({
647 path: #path_value,
648 world: #world_value,
649 with: { #(#with_entries),* },
650 });
651 }
652
653 pub fn add_components_to_linker(linker: &mut #wasvy_path::engine::Linker) {
654 type Data = ::wasmtime::component::HasSelf<#wasvy_path::host::WasmHost>;
655 #add_to_linker_path::<_, Data>(linker, |state| state)
656 .expect("implement components interface");
657 }
658
659 impl #trait_host_path for #wasvy_path::host::WasmHost {}
660
661 #(#impls)*
662 };
663
664 Ok(expanded)
665}
666
667fn expand_guest_type_paths(args: GuestTypePathsArgs) -> syn::Result<proc_macro2::TokenStream> {
668 let path_value = resolve_wit_path(&args.path);
669 let package_value = args.package.value();
670 let interface_value = args.interface.value();
671 let module = args.module;
672
673 let (namespace, name) = package_value
674 .split_once(':')
675 .ok_or_else(|| syn::Error::new(args.package.span(), "package must be `namespace:name`"))?;
676
677 let mut resolve = Resolve::default();
678 resolve
679 .push_path(&path_value)
680 .map_err(|err| syn::Error::new(args.path.span(), err.to_string()))?;
681
682 let package_id = resolve
683 .packages
684 .iter()
685 .find_map(|(id, pkg)| {
686 if pkg.name.namespace == namespace && pkg.name.name == name {
687 Some(id)
688 } else {
689 None
690 }
691 })
692 .ok_or_else(|| syn::Error::new(args.package.span(), "package not found"))?;
693
694 let interface_id = resolve
695 .interfaces
696 .iter()
697 .find_map(|(id, iface)| {
698 if iface.name.as_deref() == Some(&interface_value) && iface.package == Some(package_id)
699 {
700 Some(id)
701 } else {
702 None
703 }
704 })
705 .ok_or_else(|| syn::Error::new(args.interface.span(), "interface not found"))?;
706
707 let interface = &resolve.interfaces[interface_id];
708 let mut impls = Vec::new();
709
710 for (name, type_id) in interface.types.iter() {
711 let type_def = &resolve.types[*type_id];
712 if !matches!(type_def.kind, TypeDefKind::Resource) {
713 continue;
714 }
715 let type_path = extract_wit_type_path(&type_def.docs).ok_or_else(|| {
716 syn::Error::new(
717 args.interface.span(),
718 format!("resource `{name}` missing wasvy:type-path doc"),
719 )
720 })?;
721
722 let type_ident = format_ident!("{}", upper_camel(name));
723 let type_path_lit = syn::LitStr::new(&type_path, proc_macro2::Span::call_site());
724 impls.push(quote! {
725 impl #module::#type_ident {
726 pub const TYPE_PATH: &'static str = #type_path_lit;
727
728 pub fn type_path() -> String {
729 Self::TYPE_PATH.to_string()
730 }
731
732 pub fn type_path_str() -> &'static str {
733 Self::TYPE_PATH
734 }
735 }
736 });
737 }
738
739 Ok(quote! {
740 #(#impls)*
741 })
742}
743
744fn expand_guest_bindings(
745 args: GuestBindingsArgs,
746 input_tokens: proc_macro2::TokenStream,
747) -> syn::Result<proc_macro2::TokenStream> {
748 let mut resolve = Resolve::default();
749 for path in &args.paths {
750 let resolved = resolve_wit_path(path);
751 resolve
752 .push_path(&resolved)
753 .map_err(|err| syn::Error::new(path.span(), err.to_string()))?;
754 }
755
756 let mut impls = Vec::new();
757
758 for (_id, interface) in resolve.interfaces.iter() {
759 let Some(package_id) = interface.package else {
760 continue;
761 };
762 let package = &resolve.packages[package_id];
763 let namespace = rust_ident(&package.name.namespace);
764 let name = rust_ident(&package.name.name);
765 let interface_name = rust_ident(interface.name.as_deref().unwrap_or("components"));
766 let module = quote!(self::#namespace::#name::#interface_name);
767
768 for (resource_name, type_id) in interface.types.iter() {
769 let type_def = &resolve.types[*type_id];
770 if !matches!(type_def.kind, TypeDefKind::Resource) {
771 continue;
772 }
773 let Some(type_path) = extract_wit_type_path(&type_def.docs) else {
774 continue;
775 };
776 let type_ident = format_ident!("{}", upper_camel(resource_name));
777 let type_path_lit = syn::LitStr::new(&type_path, proc_macro2::Span::call_site());
778 impls.push(quote! {
779 impl #module::#type_ident {
780 pub const TYPE_PATH: &'static str = #type_path_lit;
781
782 pub fn type_path() -> String {
783 Self::TYPE_PATH.to_string()
784 }
785
786 pub fn type_path_str() -> &'static str {
787 Self::TYPE_PATH
788 }
789 }
790 });
791 }
792 }
793
794 Ok(quote! {
795 wit_bindgen::generate!(#input_tokens);
796 #(#impls)*
797 })
798}
799
800fn expand_include_components(args: IncludeComponentsArgs) -> syn::Result<proc_macro2::TokenStream> {
801 let base = resolve_wit_path(&args.path);
802 let base = PathBuf::from(base);
803 let mut files = Vec::new();
804 collect_rs_files(&base, &mut files)
805 .map_err(|err| syn::Error::new(args.path.span(), err.to_string()))?;
806
807 let mut root = ModuleNode::default();
808 for path in files.iter() {
809 let Ok(contents) = std::fs::read_to_string(path) else {
810 continue;
811 };
812 if !contains_wasvy_attr(&contents) {
813 continue;
814 }
815 let segments =
816 module_segments(&base, path).map_err(|err| syn::Error::new(args.path.span(), err))?;
817 root.insert(&segments, path.clone());
818 }
819
820 let rendered = render_modules(&root);
821 Ok(rendered)
822}
823
824fn render_params(
825 resolve: &Resolve,
826 params: &[wit_parser::Param],
827 wasvy_path: &proc_macro2::TokenStream,
828 is_constructor: bool,
829) -> proc_macro2::TokenStream {
830 let mut out = Vec::new();
831 if !is_constructor {
832 out.push(
833 quote!(component: ::wasmtime::component::Resource<#wasvy_path::host::WasmComponent>),
834 );
835 }
836 for param in params.iter().filter(|param| param.name != "self") {
837 let ident = rust_ident(¶m.name);
838 let ty_tokens = ty_to_tokens(resolve, ¶m.ty, wasvy_path);
839 out.push(quote!(#ident: #ty_tokens));
840 }
841 quote!(#(#out),*)
842}
843
844fn render_return(
845 resolve: &Resolve,
846 result: Option<&wit_parser::Type>,
847 wasvy_path: &proc_macro2::TokenStream,
848) -> proc_macro2::TokenStream {
849 match result {
850 None => quote!(),
851 Some(ty) => {
852 let tokens = ty_to_tokens(resolve, ty, wasvy_path);
853 quote!(-> #tokens)
854 }
855 }
856}
857
858fn render_invoke_body(
859 method: &str,
860 params: &[wit_parser::Param],
861 result: Option<&wit_parser::Type>,
862 wasvy_path: &proc_macro2::TokenStream,
863) -> proc_macro2::TokenStream {
864 let arg_idents: Vec<Ident> = params
865 .iter()
866 .filter(|param| param.name != "self")
867 .map(|param| rust_ident(¶m.name))
868 .collect();
869 let args_expr = if arg_idents.is_empty() {
870 quote!(())
871 } else {
872 quote!((#(#arg_idents),*,))
873 };
874 let method_lit = syn::LitStr::new(method, proc_macro2::Span::call_site());
875 match result {
876 None => quote! {
877 #[allow(unused_imports)]
878 use #wasvy_path::serialize::*;
879 let params = wasvy_encode(&#args_expr).expect("serialize params");
881 let _ = #wasvy_path::host::invoke_component_method(self, component, #method_lit, ¶ms)
882 .expect("invoke method");
883 },
884 Some(_) => quote! {
885 #[allow(unused_imports)]
886 use #wasvy_path::serialize::*;
887 let params = wasvy_encode(&#args_expr).expect("serialize params");
889 let output = #wasvy_path::host::invoke_component_method(self, component, #method_lit, ¶ms)
890 .expect("invoke method");
891 wasvy_decode(&output).expect("deserialize")
892 },
893 }
894}
895
896fn ty_to_tokens(
897 resolve: &Resolve,
898 ty: &wit_parser::Type,
899 wasvy_path: &proc_macro2::TokenStream,
900) -> proc_macro2::TokenStream {
901 match ty {
902 wit_parser::Type::Bool => quote!(bool),
903 wit_parser::Type::U8 => quote!(u8),
904 wit_parser::Type::U16 => quote!(u16),
905 wit_parser::Type::U32 => quote!(u32),
906 wit_parser::Type::U64 => quote!(u64),
907 wit_parser::Type::S8 => quote!(i8),
908 wit_parser::Type::S16 => quote!(i16),
909 wit_parser::Type::S32 => quote!(i32),
910 wit_parser::Type::S64 => quote!(i64),
911 wit_parser::Type::F32 => quote!(f32),
912 wit_parser::Type::F64 => quote!(f64),
913 wit_parser::Type::Char => quote!(char),
914 wit_parser::Type::String => quote!(String),
915 wit_parser::Type::Id(id) => match &resolve.types[*id].kind {
916 TypeDefKind::Resource => {
917 quote!(::wasmtime::component::Resource<#wasvy_path::host::WasmComponent>)
918 }
919 TypeDefKind::Handle(handle) => match handle {
920 wit_parser::Handle::Borrow(_) | wit_parser::Handle::Own(_) => {
921 quote!(::wasmtime::component::Resource<#wasvy_path::host::WasmComponent>)
922 }
923 },
924 TypeDefKind::Option(inner) => {
925 let inner = ty_to_tokens(resolve, inner, wasvy_path);
926 quote!(Option<#inner>)
927 }
928 TypeDefKind::List(inner) => {
929 let inner = ty_to_tokens(resolve, inner, wasvy_path);
930 quote!(Vec<#inner>)
931 }
932 _ => quote!(String),
933 },
934 wit_parser::Type::ErrorContext => quote!(String),
935 }
936}
937
938fn rust_ident(name: &str) -> Ident {
939 let mut cleaned = String::new();
940 for (i, ch) in name.chars().enumerate() {
941 let c = if ch == '-' { '_' } else { ch };
942 if i == 0 && c.is_ascii_digit() {
943 cleaned.push('_');
944 }
945 cleaned.push(c);
946 }
947 Ident::new(&cleaned, proc_macro2::Span::call_site())
948}
949
950fn method_name(name: &str) -> String {
951 let name = if let Some(pos) = name.rfind('.') {
952 &name[pos + 1..]
953 } else if let Some(pos) = name.rfind(']') {
954 &name[pos + 1..]
955 } else {
956 name
957 };
958 name.to_string()
959}
960
961fn upper_camel(name: &str) -> String {
962 let mut out = String::new();
963 let mut capitalize = true;
964 for ch in name.chars() {
965 if ch == '-' || ch == '_' {
966 capitalize = true;
967 continue;
968 }
969 if capitalize {
970 out.push(ch.to_ascii_uppercase());
971 capitalize = false;
972 } else {
973 out.push(ch);
974 }
975 }
976 if out.is_empty() {
977 "Component".to_string()
978 } else {
979 out
980 }
981}
982
983fn extract_wit_type_path(docs: &wit_parser::Docs) -> Option<String> {
984 let contents = docs.contents.as_deref()?;
985 for line in contents.lines() {
986 let line = line.trim();
987 if let Some(value) = line.strip_prefix("wasvy:type-path=") {
988 let value = value.trim();
989 if !value.is_empty() {
990 return Some(value.to_string());
991 }
992 }
993 }
994 None
995}
996
997fn extract_paths_from_stream(stream: proc_macro2::TokenStream) -> syn::Result<Vec<syn::LitStr>> {
998 let mut paths = Vec::new();
999 collect_paths(stream, &mut paths)?;
1000 if paths.is_empty() {
1001 return Err(syn::Error::new(
1002 proc_macro2::Span::call_site(),
1003 "missing `path`",
1004 ));
1005 }
1006 Ok(paths)
1007}
1008
1009fn collect_paths(
1010 stream: proc_macro2::TokenStream,
1011 paths: &mut Vec<syn::LitStr>,
1012) -> syn::Result<()> {
1013 let mut iter = stream.into_iter().peekable();
1014 while let Some(tt) = iter.next() {
1015 match tt {
1016 proc_macro2::TokenTree::Group(group) => {
1017 collect_paths(group.stream(), paths)?;
1018 }
1019 proc_macro2::TokenTree::Ident(ident) if ident == "path" => {
1020 while let Some(proc_macro2::TokenTree::Punct(p)) = iter.peek() {
1022 if p.as_char() == ':' {
1023 iter.next();
1024 break;
1025 }
1026 iter.next();
1027 }
1028
1029 let Some(next) = iter.next() else {
1030 continue;
1031 };
1032 match next {
1033 proc_macro2::TokenTree::Literal(lit) => {
1034 if let Some(value) = lit_to_litstr(&lit) {
1035 paths.push(value);
1036 } else {
1037 return Err(syn::Error::new(
1038 lit.span(),
1039 "path must be a string literal or array of string literals",
1040 ));
1041 }
1042 }
1043 proc_macro2::TokenTree::Group(group)
1044 if group.delimiter() == proc_macro2::Delimiter::Bracket =>
1045 {
1046 for elem in group.stream() {
1047 if let proc_macro2::TokenTree::Literal(lit) = elem {
1048 if let Some(value) = lit_to_litstr(&lit) {
1049 paths.push(value);
1050 } else {
1051 return Err(syn::Error::new(
1052 lit.span(),
1053 "path array entries must be string literals",
1054 ));
1055 }
1056 }
1057 }
1058 }
1059 other => {
1060 return Err(syn::Error::new(
1061 other.span(),
1062 "path must be a string literal or array of string literals",
1063 ));
1064 }
1065 }
1066 }
1067 _ => {}
1068 }
1069 }
1070 Ok(())
1071}
1072
1073fn lit_to_litstr(lit: &proc_macro2::Literal) -> Option<syn::LitStr> {
1074 syn::parse_str::<syn::LitStr>(&lit.to_string()).ok()
1075}
1076
1077fn collect_rs_files(dir: &PathBuf, out: &mut Vec<PathBuf>) -> std::io::Result<()> {
1078 if !dir.exists() {
1079 return Ok(());
1080 }
1081 for entry in std::fs::read_dir(dir)? {
1082 let entry = entry?;
1083 let path = entry.path();
1084 if path.file_name().and_then(|s| s.to_str()) == Some("target") {
1085 continue;
1086 }
1087 if path.is_dir() {
1088 collect_rs_files(&path, out)?;
1089 } else if path.extension().and_then(|s| s.to_str()) == Some("rs") {
1090 out.push(path);
1091 }
1092 }
1093 Ok(())
1094}
1095
1096fn contains_wasvy_attr(contents: &str) -> bool {
1097 contents.contains("wasvy::component")
1098 || contents.contains("wasvy::methods")
1099 || contents.contains("WasvyComponent")
1100}
1101
1102#[derive(Default)]
1103struct ModuleNode {
1104 file: Option<PathBuf>,
1105 children: std::collections::BTreeMap<String, ModuleNode>,
1106}
1107
1108impl ModuleNode {
1109 fn insert(&mut self, segments: &[String], file: PathBuf) {
1110 if segments.is_empty() {
1111 self.file = Some(file);
1112 return;
1113 }
1114 let head = segments[0].clone();
1115 let tail = &segments[1..];
1116 let child = self.children.entry(head).or_default();
1117 child.insert(tail, file);
1118 }
1119}
1120
1121fn render_modules(node: &ModuleNode) -> proc_macro2::TokenStream {
1122 render_module_node(None, node)
1123}
1124
1125fn render_module_node(name: Option<&str>, node: &ModuleNode) -> proc_macro2::TokenStream {
1126 let mut items = Vec::new();
1127 if let Some(file) = &node.file {
1128 let lit = syn::LitStr::new(&file.to_string_lossy(), proc_macro2::Span::call_site());
1129 items.push(quote! {
1130 include!(#lit);
1131 });
1132 } else {
1133 for (child_name, child) in node.children.iter() {
1134 items.push(render_module_node(Some(child_name), child));
1135 }
1136 }
1137
1138 if let Some(name) = name {
1139 let ident = rust_ident(name);
1140 quote! {
1141 mod #ident {
1142 #(#items)*
1143 }
1144 }
1145 } else {
1146 quote! {
1147 #(#items)*
1148 }
1149 }
1150}
1151
1152fn module_segments(base: &Path, file: &Path) -> Result<Vec<String>, String> {
1153 let rel = file
1154 .strip_prefix(base)
1155 .map_err(|_| "file is not under base path".to_string())?;
1156 let file_name = rel
1157 .file_name()
1158 .and_then(|s| s.to_str())
1159 .ok_or_else(|| format!("invalid utf-8 path: {}", file.to_string_lossy()))?;
1160
1161 let mut segments: Vec<String> = rel
1162 .parent()
1163 .map(|p| {
1164 p.components()
1165 .filter_map(|c| c.as_os_str().to_str().map(sanitize_ident))
1166 .collect::<Vec<_>>()
1167 })
1168 .unwrap_or_default();
1169
1170 if file_name == "lib.rs" || file_name == "main.rs" {
1171 segments.clear();
1172 return Ok(segments);
1173 }
1174
1175 if file_name != "mod.rs" {
1176 let stem_path = PathBuf::from(file_name);
1177 let stem = stem_path
1178 .file_stem()
1179 .and_then(|s| s.to_str())
1180 .ok_or_else(|| "invalid file stem".to_string())?;
1181 segments.push(sanitize_ident(stem));
1182 }
1183
1184 Ok(segments)
1185}
1186
1187fn sanitize_ident(raw: &str) -> String {
1188 let mut cleaned = String::new();
1189 for (i, ch) in raw.chars().enumerate() {
1190 let c = if ch == '-' { '_' } else { ch };
1191 if i == 0 && c.is_ascii_digit() {
1192 cleaned.push('_');
1193 }
1194 cleaned.push(c);
1195 }
1196 if cleaned.is_empty() {
1197 "_".to_string()
1198 } else {
1199 cleaned
1200 }
1201}
1202
1203fn resolve_wit_path(path: &syn::LitStr) -> String {
1204 let path_value = path.value();
1205 let resolved_path = PathBuf::from(&path_value);
1206 let resolved_path = if resolved_path.is_absolute() {
1207 resolved_path
1208 } else if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
1209 PathBuf::from(manifest_dir).join(resolved_path)
1210 } else {
1211 resolved_path
1212 };
1213 if resolved_path.exists() {
1214 return resolved_path.to_string_lossy().to_string();
1215 }
1216
1217 if let Some(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR").ok().map(PathBuf::from) {
1218 let candidates = [
1219 manifest_dir.join("target/wasvy/components.wit"),
1220 manifest_dir.join("../target/wasvy/components.wit"),
1221 manifest_dir.join("../../target/wasvy/components.wit"),
1222 ];
1223 for candidate in candidates {
1224 if candidate.exists() {
1225 return candidate.to_string_lossy().to_string();
1226 }
1227 }
1228 }
1229
1230 resolved_path.to_string_lossy().to_string()
1231}
1232
1233fn resolve_wit_path_with_fallbacks(path: &syn::LitStr) -> String {
1234 let resolved_path = PathBuf::from(resolve_wit_path(path));
1235 if resolved_path.exists() {
1236 return resolved_path.to_string_lossy().to_string();
1237 }
1238
1239 if let Some(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR").ok().map(PathBuf::from) {
1240 let candidates = [
1241 manifest_dir.join("target/wasvy/components.wit"),
1242 manifest_dir.join("../target/wasvy/components.wit"),
1243 manifest_dir.join("../../target/wasvy/components.wit"),
1244 ];
1245 for candidate in candidates {
1246 if candidate.exists() {
1247 return candidate.to_string_lossy().to_string();
1248 }
1249 }
1250 }
1251
1252 resolved_path.to_string_lossy().to_string()
1253}