1#![forbid(unsafe_code)]
2use proc_macro::TokenStream;
3use quote::{format_ident, quote};
4use syn::{
5 parse_macro_input, spanned::Spanned, Attribute, FnArg, GenericArgument, Ident, ImplItem,
6 ImplItemFn, ItemImpl, ItemTrait, Pat, PathArguments, ReturnType, TraitItem, Type, TypePath,
7};
8
9fn to_snake_case(s: &str) -> String {
12 let mut result = String::new();
13 let chars: Vec<char> = s.chars().collect();
14 for (i, &ch) in chars.iter().enumerate() {
15 if ch.is_uppercase() {
16 if i > 0 {
20 let prev_lower = chars[i - 1].is_lowercase();
21 let next_lower = chars.get(i + 1).is_some_and(|c| c.is_lowercase());
22 if prev_lower || next_lower {
23 result.push('_');
24 }
25 }
26 result.push(ch.to_ascii_lowercase());
27 } else {
28 result.push(ch);
29 }
30 }
31 result
32}
33
34fn to_pascal_case(s: &str) -> String {
35 s.split('_')
36 .filter(|part| !part.is_empty())
37 .map(|part| {
38 let mut chars = part.chars();
39 match chars.next() {
40 None => String::new(),
41 Some(c) => c.to_uppercase().to_string() + chars.as_str(),
42 }
43 })
44 .collect()
45}
46
47fn strip_protocol_suffix(name: &str) -> String {
48 name.strip_suffix("Protocol").unwrap_or(name).to_string()
49}
50
51fn conditional_timeout(meta: &syn::Meta) -> bool {
55 if let syn::Meta::List(list) = meta {
56 if list.path.is_ident("cfg_attr") {
57 use syn::parse::Parser;
58 let arguments =
59 syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated;
60 if let Ok(arguments) = arguments.parse2(list.tokens.clone()) {
61 return arguments.iter().skip(1).any(|meta| {
62 meta.path().is_ident("request_timeout") || conditional_timeout(meta)
63 });
64 }
65 }
66 }
67 false
68}
69
70enum MethodKind {
71 Send,
72 Request(Box<Type>),
73}
74
75#[derive(Clone, Copy)]
76enum RuntimeMode {
77 Tasks,
78 Threads,
79}
80
81fn classify_return_type(ret: &ReturnType) -> Result<MethodKind, &Type> {
82 match ret {
83 ReturnType::Default => Ok(MethodKind::Send),
84 ReturnType::Type(_, ty) => {
85 if is_unit_type(ty) {
86 return Ok(MethodKind::Send);
87 }
88 if let Some(inner) = extract_response_inner(ty) {
89 return Ok(MethodKind::Request(inner));
90 }
91 if let Some(inner) = extract_result_inner(ty) {
92 if is_unit_type(&inner) {
93 return Ok(MethodKind::Send);
94 }
95 return Err(ty);
98 }
99 Err(ty)
100 }
101 }
102}
103
104fn extract_response_inner(ty: &Type) -> Option<Box<Type>> {
105 if let Type::Path(TypePath { path, .. }) = ty {
106 let seg = path.segments.last()?;
107 if seg.ident == "Response" {
108 if let PathArguments::AngleBracketed(args) = &seg.arguments {
109 if let Some(GenericArgument::Type(inner)) = args.args.first() {
110 return Some(Box::new(inner.clone()));
111 }
112 }
113 }
114 }
115 None
116}
117
118fn extract_result_inner(ty: &Type) -> Option<Box<Type>> {
119 if let Type::Path(TypePath { path, .. }) = ty {
120 let seg = path.segments.last()?;
121 if seg.ident == "Result" {
122 if let PathArguments::AngleBracketed(args) = &seg.arguments {
123 if let Some(GenericArgument::Type(inner)) = args.args.first() {
124 return Some(Box::new(inner.clone()));
125 }
126 }
127 }
128 }
129 None
130}
131
132fn unsupported_param_type(ty: &Type) -> Option<syn::Error> {
135 const OWNED: &str = "protocol parameters become message fields and must be owned ('static); \
136 use an owned type such as String or Arc<T> instead of a borrow";
137 match ty {
138 Type::Reference(r) => match &r.lifetime {
139 Some(lifetime) if lifetime.ident == "static" => unsupported_param_type(&r.elem),
140 _ => Some(syn::Error::new_spanned(r, OWNED)),
141 },
142 Type::ImplTrait(t) => Some(syn::Error::new_spanned(
143 t,
144 "`impl Trait` parameters are not supported in protocol methods; \
145 use a concrete type or Box<dyn Trait>",
146 )),
147 Type::Path(TypePath { qself, path }) => {
148 if let Some(error) = qself.as_ref().and_then(|q| unsupported_param_type(&q.ty)) {
149 return Some(error);
150 }
151 path.segments
152 .iter()
153 .find_map(|segment| match &segment.arguments {
154 PathArguments::AngleBracketed(args) => {
155 args.args.iter().find_map(|arg| match arg {
156 GenericArgument::Type(inner) => unsupported_param_type(inner),
157 GenericArgument::Lifetime(lifetime) if lifetime.ident != "static" => {
158 Some(syn::Error::new_spanned(lifetime, OWNED))
159 }
160 _ => None,
161 })
162 }
163 _ => None,
164 })
165 }
166 Type::Tuple(t) => t.elems.iter().find_map(unsupported_param_type),
167 Type::Array(a) => unsupported_param_type(&a.elem),
168 Type::Slice(s) => unsupported_param_type(&s.elem),
169 Type::Paren(p) => unsupported_param_type(&p.elem),
170 Type::Group(g) => unsupported_param_type(&g.elem),
171 _ => None,
173 }
174}
175
176fn is_unit_type(ty: &Type) -> bool {
177 if let Type::Tuple(tuple) = ty {
178 return tuple.elems.is_empty();
179 }
180 false
181}
182
183fn is_prelude_or_primitive(name: &str) -> bool {
186 matches!(
187 name,
188 "bool" | "char" | "str"
190 | "i8" | "i16" | "i32" | "i64" | "i128" | "isize"
191 | "u8" | "u16" | "u32" | "u64" | "u128" | "usize"
192 | "f32" | "f64"
193 | "Box" | "String" | "Vec"
195 | "Option" | "Some" | "None"
196 | "Result" | "Ok" | "Err"
197 | "ToString" | "ToOwned"
198 )
199}
200
201fn qualify_type_with_super(ty: &Type) -> Type {
209 match ty {
210 Type::Path(TypePath { qself, path }) => {
211 if qself.is_some() || path.leading_colon.is_some() {
213 return ty.clone();
214 }
215 if let Some(first) = path.segments.first() {
216 let s = first.ident.to_string();
217 if matches!(
218 s.as_str(),
219 "crate" | "super" | "self" | "std" | "core" | "alloc"
220 ) {
221 return ty.clone();
222 }
223 }
224
225 let qualified_segments: syn::punctuated::Punctuated<_, _> = path
227 .segments
228 .iter()
229 .map(|seg| {
230 let mut new_seg = seg.clone();
231 if let PathArguments::AngleBracketed(ref mut args) = new_seg.arguments {
232 for arg in &mut args.args {
233 if let GenericArgument::Type(ref mut inner) = arg {
234 *inner = qualify_type_with_super(inner);
235 }
236 }
237 }
238 new_seg
239 })
240 .collect();
241
242 if let Some(first) = path.segments.first() {
244 if is_prelude_or_primitive(&first.ident.to_string()) {
245 return Type::Path(TypePath {
246 qself: None,
247 path: syn::Path {
248 leading_colon: None,
249 segments: qualified_segments,
250 },
251 });
252 }
253 }
254
255 let mut segments = syn::punctuated::Punctuated::new();
257 segments.push(syn::PathSegment {
258 ident: format_ident!("super"),
259 arguments: PathArguments::None,
260 });
261 for seg in qualified_segments {
262 segments.push(seg);
263 }
264
265 Type::Path(TypePath {
266 qself: None,
267 path: syn::Path {
268 leading_colon: None,
269 segments,
270 },
271 })
272 }
273 Type::Reference(r) => {
274 let mut new = r.clone();
275 new.elem = Box::new(qualify_type_with_super(&r.elem));
276 Type::Reference(new)
277 }
278 Type::Tuple(t) => {
279 let mut new = t.clone();
280 for elem in &mut new.elems {
281 *elem = qualify_type_with_super(elem);
282 }
283 Type::Tuple(new)
284 }
285 Type::Array(a) => {
286 let mut new = a.clone();
287 *new.elem = qualify_type_with_super(&a.elem);
288 Type::Array(new)
289 }
290 Type::Slice(s) => {
291 let mut new = s.clone();
292 *new.elem = qualify_type_with_super(&s.elem);
293 Type::Slice(new)
294 }
295 Type::Paren(p) => {
296 let mut new = p.clone();
297 *new.elem = qualify_type_with_super(&p.elem);
298 Type::Paren(new)
299 }
300 Type::TraitObject(t) => {
301 let mut new = t.clone();
302 for bound in &mut new.bounds {
303 if let syn::TypeParamBound::Trait(tb) = bound {
304 qualify_path_with_super(&mut tb.path);
305 }
306 }
307 Type::TraitObject(new)
308 }
309 Type::ImplTrait(t) => {
310 let mut new = t.clone();
311 for bound in &mut new.bounds {
312 if let syn::TypeParamBound::Trait(tb) = bound {
313 qualify_path_with_super(&mut tb.path);
314 }
315 }
316 Type::ImplTrait(new)
317 }
318 Type::BareFn(f) => {
319 let mut new = f.clone();
320 for arg in &mut new.inputs {
321 arg.ty = qualify_type_with_super(&arg.ty);
322 }
323 if let ReturnType::Type(_, ref mut ty) = new.output {
324 **ty = qualify_type_with_super(ty);
325 }
326 Type::BareFn(new)
327 }
328 _ => ty.clone(),
329 }
330}
331
332fn qualify_path_with_super(path: &mut syn::Path) {
335 for seg in &mut path.segments {
336 match &mut seg.arguments {
337 PathArguments::AngleBracketed(ref mut args) => {
338 for arg in &mut args.args {
339 if let GenericArgument::Type(ref mut inner) = arg {
340 *inner = qualify_type_with_super(inner);
341 }
342 }
343 }
344 PathArguments::Parenthesized(ref mut args) => {
345 for input in &mut args.inputs {
346 *input = qualify_type_with_super(input);
347 }
348 if let syn::ReturnType::Type(_, ref mut ty) = args.output {
349 **ty = qualify_type_with_super(ty);
350 }
351 }
352 PathArguments::None => {}
353 }
354 }
355}
356
357struct ProtocolInfo<'a> {
358 trait_name: &'a Ident,
359 mod_name: &'a Ident,
360 ref_name: &'a Ident,
361 converter_trait: &'a Ident,
362 converter_method: &'a Ident,
363 target_trait: &'a Ident,
364}
365
366fn transport_signature(
368 info: &ProtocolInfo,
369 method: &ProtocolMethodInfo,
370 index: usize,
371) -> (Ident, proc_macro2::TokenStream) {
372 let name = format_ident!("__dispatch_{}", index);
373 let module = info.mod_name;
374 let message = &method.struct_name;
375 let (timeout, result) = match &method.kind {
376 MethodKind::Send => (
377 quote! {},
378 quote! { ::std::result::Result<(), spawned_concurrency::ActorError> },
379 ),
380 MethodKind::Request(result) => (
381 quote! { , timeout: ::std::time::Duration },
382 quote! { spawned_concurrency::Response<#result> },
383 ),
384 };
385 let signature =
386 quote! { fn #name(target: &Self, message: #module::#message #timeout) -> #result };
387 (name, signature)
388}
389
390fn generate_target_impl(
393 info: &ProtocolInfo,
394 methods: &[ProtocolMethodInfo],
395 runtime_path: &proc_macro2::TokenStream,
396 mode: RuntimeMode,
397) -> proc_macro2::TokenStream {
398 let ProtocolInfo {
399 trait_name,
400 mod_name,
401 target_trait,
402 ..
403 } = info;
404
405 let handler_bounds: Vec<_> = methods
407 .iter()
408 .filter(|m| m.cfg_attrs.is_empty())
409 .map(|m| {
410 let sn = &m.struct_name;
411 quote! { #runtime_path::Handler<#mod_name::#sn> }
412 })
413 .collect();
414
415 let mut cfg_groups: Vec<(String, Vec<&ProtocolMethodInfo>)> = Vec::new();
418 for m in methods.iter().filter(|m| !m.cfg_attrs.is_empty()) {
419 let key: String = m
420 .cfg_attrs
421 .iter()
422 .map(|a| quote!(#a).to_string())
423 .collect::<Vec<_>>()
424 .join(",");
425 if let Some(group) = cfg_groups.iter_mut().find(|(k, _)| k == &key) {
426 group.1.push(m);
427 } else {
428 cfg_groups.push((key, vec![m]));
429 }
430 }
431
432 let mode_suffix = match mode {
433 RuntimeMode::Tasks => format_ident!("Tasks"),
434 RuntimeMode::Threads => format_ident!("Threads"),
435 };
436
437 let mut marker_trait_defs = Vec::new();
438 let mut marker_trait_bounds = Vec::new();
439
440 for (i, (_key, group_methods)) in cfg_groups.iter().enumerate() {
441 let marker_name = format_ident!("__{}Cfg{}{}", trait_name, i, mode_suffix);
442 let cfg_attrs = &group_methods[0].cfg_attrs;
443 let group_handler_bounds: Vec<_> = group_methods
444 .iter()
445 .map(|m| {
446 let sn = &m.struct_name;
447 quote! { #runtime_path::Handler<#mod_name::#sn> }
448 })
449 .collect();
450
451 let cfg_predicates: Vec<proc_macro2::TokenStream> = cfg_attrs
454 .iter()
455 .filter(|a| a.path().is_ident("cfg"))
456 .filter_map(|a| a.parse_args::<proc_macro2::TokenStream>().ok())
457 .collect();
458
459 let (positive_cfg, negated_cfg) = if cfg_predicates.len() == 1 {
460 let pred = &cfg_predicates[0];
461 (quote! { #[cfg(#pred)] }, quote! { #[cfg(not(#pred))] })
462 } else {
463 (
464 quote! { #[cfg(all(#(#cfg_predicates),*))] },
465 quote! { #[cfg(not(all(#(#cfg_predicates),*)))] },
466 )
467 };
468
469 marker_trait_defs.push(quote! {
470 #positive_cfg
471 #[doc(hidden)]
472 trait #marker_name: #(#group_handler_bounds)+* {}
473 #positive_cfg
474 impl<__T: #(#group_handler_bounds)+*> #marker_name for __T {}
475
476 #negated_cfg
477 #[doc(hidden)]
478 trait #marker_name {}
479 #negated_cfg
480 impl<__T> #marker_name for __T {}
481 });
482
483 marker_trait_bounds.push(quote! { + #marker_name });
484 }
485
486 let method_impls = methods.iter().enumerate().map(|(index, m)| {
487 let (_, signature) = transport_signature(info, m, index);
488 let cfg = &m.cfg_attrs;
489 let body = match &m.kind {
490 MethodKind::Send => quote! { target.send(message) },
491 MethodKind::Request(_) => match mode {
492 RuntimeMode::Tasks => quote! { spawned_concurrency::Response::from_with_timeout(target.request_raw(message), timeout) },
493 RuntimeMode::Threads => quote! { spawned_concurrency::Response::ready(target.request_with_timeout(message, timeout)) },
494 },
495 };
496 quote! { #(#cfg)* #signature { #body } }
497 });
498 quote! {
499 #(#marker_trait_defs)*
500 impl<__A: #runtime_path::Actor #(+ #handler_bounds)* #(#marker_trait_bounds)*>
501 #target_trait for #runtime_path::ActorRef<__A>
502 {
503 #(#method_impls)*
504 }
505 }
506}
507
508fn generate_protocol_adapters(
511 info: &ProtocolInfo,
512 methods: &[ProtocolMethodInfo],
513 definition: &ItemTrait,
514) -> proc_macro2::TokenStream {
515 let ProtocolInfo {
516 trait_name,
517 mod_name,
518 ref_name,
519 converter_trait,
520 converter_method,
521 target_trait,
522 } = info;
523 let signatures = methods.iter().enumerate().map(|(index, m)| {
524 let (_, signature) = transport_signature(info, m, index);
525 let cfg = &m.cfg_attrs;
526 quote! { #(#cfg)* #signature; }
527 });
528 let delegates = methods.iter().enumerate().map(|(index, m)| {
529 let (name, signature) = transport_signature(info, m, index);
530 let cfg = &m.cfg_attrs;
531 let timeout = match &m.kind { MethodKind::Send => quote! {}, MethodKind::Request(_) => quote! { , timeout } };
532 quote! {
533 #(#cfg)* #signature {
534 <__T as #target_trait>::#name(
535 spawned_concurrency::response::RequestOptions::actor_ref(target), message #timeout
536 )
537 }
538 }
539 });
540 let method_impls: Vec<_> = methods.iter().enumerate().map(|(index, m)| {
541 let method = &m.method_name;
542 let dispatch = format_ident!("__dispatch_{}", index);
543 let params = &m.params;
544 let attrs = &m.method_attrs;
545 let result = &m.ret_type;
546 let message = &m.struct_name;
547 let fields = &m.field_names;
548 let construct = if fields.is_empty() {
549 quote! { #mod_name::#message }
550 } else {
551 quote! { #mod_name::#message { #(#fields),* } }
552 };
553 let body = match &m.kind {
554 MethodKind::Send => {
555 let call = quote! { <Self as #target_trait>::#dispatch(self, #construct) };
556 let unit = match result {
557 ReturnType::Default => true,
558 ReturnType::Type(_, ty) => is_unit_type(ty),
559 };
560 if unit { quote! { let _ = #call; } } else { call }
561 }
562 MethodKind::Request(_) => {
563 let timeout = &m.timeout;
564 let binding = syn::Ident::new("__spawned_request_timeout", proc_macro2::Span::mixed_site());
565 quote! {
566 let #binding: ::std::time::Duration = match <Self as #target_trait>::__timeout_override(self) {
569 Some(timeout) => timeout,
570 None => #timeout,
571 };
572 <Self as #target_trait>::#dispatch(self, #construct, #binding)
573 }
574 }
575 };
576 quote! {
577 #(#attrs)*
578 fn #method(&self, #(#params),*) #result { #body }
579 }
580 }).collect();
581 let mut public_impls = Vec::new();
584 for (runtime, asynchronous) in [
585 (quote! { spawned_concurrency::tasks }, true),
586 (quote! { spawned_concurrency::threads }, false),
587 ] {
588 let actor = quote! { #runtime::ActorRef<__A> };
589 for target in [
590 actor.clone(),
591 quote! { spawned_concurrency::response::RequestOptions<#actor> },
592 ] {
593 let mut predicates: Vec<_> = definition
594 .generics
595 .where_clause
596 .iter()
597 .flat_map(|clause| clause.predicates.iter())
598 .map(|p| quote! { #p })
599 .collect();
600 let supertraits = &definition.supertraits;
601 if !supertraits.is_empty() {
602 predicates.push(quote! { Self: #supertraits });
603 }
604 let implementation = quote! {
605 impl<__A: #runtime::Actor> #trait_name for #target
606 where Self: #target_trait, #(#predicates,)*
607 { #(#method_impls)* }
608 impl<__A: #runtime::Actor> #converter_trait for #target
609 where Self: #target_trait + #trait_name
610 {
611 fn #converter_method(&self) -> #ref_name { ::std::sync::Arc::new(self.clone()) }
612 }
613 };
614 public_impls.push(if asynchronous {
615 quote! { spawned_concurrency::__spawned_async_items! { #implementation } }
616 } else {
617 implementation
618 });
619 }
620 }
621 quote! {
622 #[doc(hidden)]
623 trait #target_trait: Clone + Send + Sync + 'static {
624 fn __timeout_override(_target: &Self) -> Option<::std::time::Duration> { None }
625 #(#signatures)*
626 }
627 impl<__T: #target_trait> #target_trait for spawned_concurrency::response::RequestOptions<__T> {
628 fn __timeout_override(target: &Self) -> Option<::std::time::Duration> {
629 Some(spawned_concurrency::response::RequestOptions::timeout(target))
630 }
631 #(#delegates)*
632 }
633 #(#public_impls)*
634 }
635}
636
637struct ProtocolMethodInfo {
638 method_name: Ident,
639 struct_name: Ident,
640 field_names: Vec<Ident>,
641 field_types: Vec<Type>,
642 kind: MethodKind,
643 params: Vec<FnArg>,
644 ret_type: ReturnType,
645 method_attrs: Vec<Attribute>,
647 cfg_attrs: Vec<Attribute>,
648 timeout: syn::Expr,
649}
650
651#[proc_macro_attribute]
727pub fn protocol(attr: TokenStream, item: TokenStream) -> TokenStream {
728 let options = parse_macro_input!(attr with syn::punctuated::Punctuated::<syn::Meta, syn::Token![,]>::parse_terminated);
729 let mut protocol_timeout = None;
730 for option in options {
731 if !option.path().is_ident("timeout") {
732 return syn::Error::new_spanned(
733 option.path(),
734 "unsupported protocol option; expected timeout = Duration expression",
735 )
736 .to_compile_error()
737 .into();
738 }
739 let syn::Meta::NameValue(option) = option else {
740 return syn::Error::new_spanned(option, "expected `timeout = <Duration expression>`")
741 .to_compile_error()
742 .into();
743 };
744 if protocol_timeout.replace(option.value).is_some() {
745 return syn::Error::new_spanned(option.path, "duplicate protocol timeout")
746 .to_compile_error()
747 .into();
748 }
749 }
750 let mut trait_def = parse_macro_input!(item as ItemTrait);
751 let trait_name = &trait_def.ident;
752 let trait_vis = &trait_def.vis;
753
754 if !trait_def.generics.params.is_empty() {
755 return syn::Error::new_spanned(
756 &trait_def.generics,
757 "generic parameters (types, lifetimes, consts) on protocol traits are not supported",
758 )
759 .to_compile_error()
760 .into();
761 }
762
763 let base_name = strip_protocol_suffix(&trait_name.to_string());
764 let mod_name = format_ident!("{}", to_snake_case(&trait_name.to_string()));
765 let ref_name = format_ident!("{}Ref", base_name);
766 let converter_trait = format_ident!("To{}Ref", base_name);
767 let converter_method = format_ident!("to_{}_ref", to_snake_case(&base_name));
768
769 let mut methods: Vec<ProtocolMethodInfo> = Vec::new();
770
771 for item in &mut trait_def.items {
772 if !matches!(item, TraitItem::Fn(_)) {
773 return syn::Error::new_spanned(
774 item,
775 "protocol traits may only contain methods; \
776 associated types, constants, and other items are not supported",
777 )
778 .to_compile_error()
779 .into();
780 }
781 if let TraitItem::Fn(method) = item {
782 if let Some(attribute) = method.attrs.iter().find(|a| conditional_timeout(&a.meta)) {
783 return syn::Error::new_spanned(
784 attribute,
785 "conditional request_timeout is not supported; use #[request_timeout(...)] directly",
786 ).to_compile_error().into();
787 }
788 if method.sig.asyncness.is_some() {
789 return syn::Error::new_spanned(
790 &method.sig,
791 "protocol methods must not be async; \
792 use Response<T> as the return type for requests",
793 )
794 .to_compile_error()
795 .into();
796 }
797
798 match method.sig.inputs.first() {
800 Some(FnArg::Receiver(r)) if r.reference.is_some() && r.mutability.is_none() => {}
801 _ => {
802 return syn::Error::new_spanned(
803 &method.sig,
804 "protocol methods must take `&self` as the first parameter",
805 )
806 .to_compile_error()
807 .into();
808 }
809 }
810
811 let sig = &method.sig;
812 let qualifier = [
813 (sig.unsafety.as_ref().map(|t| quote!(#t)), "unsafe"),
814 (sig.abi.as_ref().map(|t| quote!(#t)), "extern"),
815 (sig.constness.as_ref().map(|t| quote!(#t)), "const"),
816 ]
817 .into_iter()
818 .find_map(|(token, name)| Some((token?, name)));
819 if let Some((token, name)) = qualifier {
820 return syn::Error::new_spanned(
821 token,
822 format!("`{name}` protocol methods are not supported; use a plain `fn`"),
823 )
824 .to_compile_error()
825 .into();
826 }
827 if !method.sig.generics.params.is_empty() {
828 return syn::Error::new_spanned(
829 &method.sig.generics,
830 "protocol methods cannot have generic parameters; \
831 each method becomes one concrete message struct",
832 )
833 .to_compile_error()
834 .into();
835 }
836 for arg in method.sig.inputs.iter().skip(1) {
837 if let FnArg::Typed(pat_type) = arg {
838 if let Some(error) = unsupported_param_type(&pat_type.ty) {
839 return error.to_compile_error().into();
840 }
841 }
842 }
843
844 let method_name = method.sig.ident.clone();
845 let struct_name = format_ident!("{}", to_pascal_case(&method_name.to_string()));
846 let conditional = |attrs: &[Attribute]| {
849 attrs
850 .iter()
851 .any(|a| a.path().is_ident("cfg") || a.path().is_ident("cfg_attr"))
852 };
853 if !conditional(&method.attrs) {
854 if let Some(previous) = methods
855 .iter()
856 .find(|m| m.cfg_attrs.is_empty() && m.struct_name == struct_name)
857 {
858 let message = if previous.method_name == method_name {
859 format!("duplicate protocol method `{method_name}`")
860 } else {
861 format!(
862 "protocol method `{method_name}` generates message struct \
863 `{struct_name}`, which `{}` already uses; rename one of them",
864 previous.method_name
865 )
866 };
867 return syn::Error::new_spanned(&method.sig.ident, message)
868 .to_compile_error()
869 .into();
870 }
871 }
872
873 let mut field_names: Vec<Ident> = Vec::new();
874 let mut field_types: Vec<Type> = Vec::new();
875 let mut params: Vec<FnArg> = Vec::new();
876
877 for arg in method.sig.inputs.iter().skip(1) {
878 if let FnArg::Typed(pat_type) = arg {
879 if let Pat::Ident(pat_ident) = &*pat_type.pat {
880 field_names.push(pat_ident.ident.clone());
881 field_types.push((*pat_type.ty).clone());
882 } else {
883 return syn::Error::new_spanned(
884 &pat_type.pat,
885 "protocol methods only support simple identifier patterns \
886 (e.g., `name: Type`)",
887 )
888 .to_compile_error()
889 .into();
890 }
891 }
892 params.push(arg.clone());
893 }
894
895 let kind = match classify_return_type(&method.sig.output) {
896 Ok(kind) => kind,
897 Err(ty) => {
898 return syn::Error::new_spanned(
899 ty,
900 "unsupported return type in protocol method; \
901 use Response<T> for requests (works in both async and sync modes), \
902 Result<(), ActorError> for sends, or no return type for fire-and-forget",
903 )
904 .to_compile_error()
905 .into();
906 }
907 };
908
909 let mut method_timeout = None;
910 for attr in method
911 .attrs
912 .iter()
913 .filter(|a| a.path().is_ident("request_timeout"))
914 {
915 if !matches!(kind, MethodKind::Request(_)) {
916 return syn::Error::new_spanned(
917 attr,
918 "request_timeout is only valid on Response<T> methods",
919 )
920 .to_compile_error()
921 .into();
922 }
923 let expression = match attr.parse_args::<syn::Expr>() {
924 Ok(expression) => expression,
925 Err(error) => return error.to_compile_error().into(),
926 };
927 if method_timeout.replace(expression).is_some() {
928 return syn::Error::new_spanned(attr, "duplicate request_timeout")
929 .to_compile_error()
930 .into();
931 }
932 }
933 method
934 .attrs
935 .retain(|a| !a.path().is_ident("request_timeout"));
936 let timeout = method_timeout
937 .or_else(|| protocol_timeout.clone())
938 .unwrap_or_else(
939 || syn::parse_quote! { spawned_concurrency::response::DEFAULT_REQUEST_TIMEOUT },
940 );
941
942 let method_attrs: Vec<Attribute> = method
943 .attrs
944 .iter()
945 .filter(|a| {
946 a.path().is_ident("doc")
947 || a.path().is_ident("cfg")
948 || a.path().is_ident("cfg_attr")
949 })
950 .cloned()
951 .collect();
952 let cfg_attrs: Vec<Attribute> = method
953 .attrs
954 .iter()
955 .filter(|a| a.path().is_ident("cfg") || a.path().is_ident("cfg_attr"))
956 .cloned()
957 .collect();
958
959 methods.push(ProtocolMethodInfo {
960 method_name,
961 struct_name,
962 field_names,
963 field_types,
964 kind,
965 params,
966 ret_type: method.sig.output.clone(),
967 method_attrs,
968 cfg_attrs,
969 timeout,
970 });
971 }
972 }
973
974 let msg_structs: Vec<_> = methods
979 .iter()
980 .map(|m| {
981 let struct_name = &m.struct_name;
982 let field_names = &m.field_names;
983 let qualified_field_types: Vec<Type> =
984 m.field_types.iter().map(qualify_type_with_super).collect();
985 let method_attrs = &m.method_attrs;
986 let cfg_attrs = &m.cfg_attrs;
987 let msg_result_ty: Type = match &m.kind {
988 MethodKind::Send => syn::parse_quote! { () },
989 MethodKind::Request(inner) => qualify_type_with_super(inner),
990 };
991
992 if field_names.is_empty() {
993 quote! {
994 #(#method_attrs)*
995 #[derive(Clone)]
996 pub struct #struct_name;
997 #(#cfg_attrs)*
998 impl Message for #struct_name {
999 type Result = #msg_result_ty;
1000 }
1001 }
1002 } else {
1003 quote! {
1004 #(#method_attrs)*
1005 pub struct #struct_name {
1006 #(pub #field_names: #qualified_field_types,)*
1007 }
1008 #(#cfg_attrs)*
1009 impl Message for #struct_name {
1010 type Result = #msg_result_ty;
1011 }
1012 }
1013 }
1014 })
1015 .collect();
1016
1017 let target_trait = format_ident!("__{}Target", trait_name);
1019 let tasks = quote! { spawned_concurrency::tasks };
1020 let threads = quote! { spawned_concurrency::threads };
1021 let proto_info = ProtocolInfo {
1022 trait_name,
1023 mod_name: &mod_name,
1024 ref_name: &ref_name,
1025 converter_trait: &converter_trait,
1026 converter_method: &converter_method,
1027 target_trait: &target_trait,
1028 };
1029 let tasks_impl = generate_target_impl(&proto_info, &methods, &tasks, RuntimeMode::Tasks);
1030 let threads_impl = generate_target_impl(&proto_info, &methods, &threads, RuntimeMode::Threads);
1031 let adapters = generate_protocol_adapters(&proto_info, &methods, &trait_def);
1032 let blanket_impls = quote! {
1033 #adapters
1034 spawned_concurrency::__spawned_async_items! { #tasks_impl }
1035 #threads_impl
1036 };
1037
1038 let ref_doc = format!(
1039 "Type-erased reference to any actor implementing [`{trait_name}`].\n\n\
1040 Use this type to store protocol references without depending on the concrete actor type."
1041 );
1042
1043 let output = quote! {
1046 #[allow(dead_code)]
1047 #trait_def
1048
1049 #[doc = #ref_doc]
1050 #trait_vis type #ref_name = ::std::sync::Arc<dyn #trait_name>;
1051
1052 #trait_vis mod #mod_name {
1053 use super::*;
1054 use spawned_concurrency::message::Message;
1055 #(#msg_structs)*
1056 }
1057
1058 #trait_vis trait #converter_trait {
1059 fn #converter_method(&self) -> #ref_name;
1060 }
1061
1062 impl #converter_trait for #ref_name {
1063 fn #converter_method(&self) -> #ref_name {
1064 ::std::sync::Arc::clone(self)
1065 }
1066 }
1067
1068 #blanket_impls
1069 };
1070
1071 output.into()
1072}
1073
1074fn is_handler_attribute(attr: &Attribute) -> bool {
1075 let path = attr.path();
1076 path.is_ident("handler") || path.is_ident("send_handler") || path.is_ident("request_handler")
1077}
1078
1079fn is_actor_method_attribute(attr: &Attribute) -> bool {
1080 let path = attr.path();
1081 is_handler_attribute(attr)
1082 || path.is_ident("started")
1083 || path.is_ident("stopped")
1084 || path.is_ident("exit_received")
1085}
1086
1087#[proc_macro_attribute]
1164pub fn actor(attr: TokenStream, item: TokenStream) -> TokenStream {
1165 let mut impl_block = parse_macro_input!(item as ItemImpl);
1166
1167 if let Some((_, path, _)) = &impl_block.trait_ {
1168 return syn::Error::new_spanned(
1169 path,
1170 "#[actor] must be applied to an inherent impl block (`impl MyActor { ... }`), \
1171 not a trait impl",
1172 )
1173 .to_compile_error()
1174 .into();
1175 }
1176
1177 let mut runtime_methods = impl_block.items.iter().filter_map(|item| match item {
1181 ImplItem::Fn(method)
1182 if method.attrs.iter().any(is_actor_method_attribute)
1183 && !method.attrs.iter().any(|a| a.path().is_ident("cfg"))
1184 && !matches!(&method.sig.output, ReturnType::Type(_, ty) if matches!(**ty, Type::ImplTrait(_))) =>
1185 {
1186 Some(&method.sig)
1187 }
1188 _ => None,
1189 });
1190 if let Some(first) = runtime_methods.next() {
1191 let asynchronous = first.asyncness.is_some();
1192 if let Some(other) = runtime_methods.find(|sig| sig.asyncness.is_some() != asynchronous) {
1193 return syn::Error::new_spanned(
1194 other,
1195 "cannot mix async and sync handlers or lifecycle hooks in one #[actor] block; \
1196 tasks actors use `async fn` throughout, threads actors use plain `fn`",
1197 )
1198 .to_compile_error()
1199 .into();
1200 }
1201 }
1202
1203 let self_ty = &impl_block.self_ty;
1204 let (impl_generics, _, where_clause) = impl_block.generics.split_for_impl();
1205
1206 let bridge_traits: Vec<Ident> = if attr.is_empty() {
1208 Vec::new()
1209 } else {
1210 fn protocol_name(input: syn::parse::ParseStream) -> syn::Result<Ident> {
1213 let path: syn::Path = input.parse()?;
1214 path.get_ident().cloned().ok_or_else(|| {
1215 syn::Error::new_spanned(
1216 &path,
1217 "protocol must be named by a single identifier; \
1218 import the trait first (`use path::to::Protocol;`)",
1219 )
1220 })
1221 }
1222 let parser = |input: syn::parse::ParseStream| -> syn::Result<Vec<Ident>> {
1223 let mut protocols = Vec::new();
1224 while !input.is_empty() {
1225 let key: Ident = input.parse()?;
1226 if key != "protocol" {
1227 return Err(syn::Error::new(
1228 key.span(),
1229 "unknown parameter, expected `protocol`",
1230 ));
1231 }
1232 if input.peek(syn::Token![=]) {
1233 let _: syn::Token![=] = input.parse()?;
1235 protocols.push(protocol_name(input)?);
1236 } else if input.peek(syn::token::Paren) {
1237 let content;
1239 syn::parenthesized!(content in input);
1240 let punctuated = content.parse_terminated(protocol_name, syn::Token![,])?;
1241 protocols.extend(punctuated);
1242 } else {
1243 return Err(syn::Error::new(
1244 key.span(),
1245 "expected `protocol = Trait` or `protocol(TraitA, TraitB)`",
1246 ));
1247 }
1248 if !input.is_empty() {
1249 let _: syn::Token![,] = input.parse().map_err(|e| {
1250 syn::Error::new(e.span(), "expected `,` between #[actor] parameters")
1251 })?;
1252 }
1253 }
1254 Ok(protocols)
1255 };
1256 match syn::parse::Parser::parse(parser, attr) {
1257 Ok(traits) => traits,
1258 Err(e) => return e.to_compile_error().into(),
1259 }
1260 };
1261
1262 let mut started_method: Option<ImplItemFn> = None;
1264 let mut stopped_method: Option<ImplItemFn> = None;
1265 let mut exit_method: Option<ImplItemFn> = None;
1266 let mut has_async = false;
1267
1268 let mut items_to_keep = Vec::new();
1269 for item in impl_block.items.drain(..) {
1270 if let ImplItem::Fn(ref method) = item {
1271 let is_started = method.attrs.iter().any(|a| a.path().is_ident("started"));
1272 let is_stopped = method.attrs.iter().any(|a| a.path().is_ident("stopped"));
1273
1274 let is_exit = method
1275 .attrs
1276 .iter()
1277 .any(|a| a.path().is_ident("exit_received"));
1278 if is_exit {
1279 if exit_method.is_some() {
1280 return syn::Error::new_spanned(
1281 &method.sig,
1282 "only one #[exit_received] method is allowed per actor",
1283 )
1284 .to_compile_error()
1285 .into();
1286 }
1287 if is_started
1288 || is_stopped
1289 || method.attrs.iter().any(|a| {
1290 a.path().is_ident("handler")
1291 || a.path().is_ident("send_handler")
1292 || a.path().is_ident("request_handler")
1293 })
1294 {
1295 return syn::Error::new_spanned(&method.sig, "#[exit_received] cannot be combined with other lifecycle or handler attributes").to_compile_error().into();
1296 }
1297 if method.sig.inputs.len() != 3
1298 || !matches!(method.sig.inputs.first(), Some(FnArg::Receiver(r)) if r.reference.is_some() && r.mutability.is_some())
1299 {
1300 return syn::Error::new_spanned(
1301 &method.sig,
1302 "#[exit_received] must take (&mut self, Exit, &Context<Self>)",
1303 )
1304 .to_compile_error()
1305 .into();
1306 }
1307 let mut m = method.clone();
1308 m.attrs.retain(|a| !a.path().is_ident("exit_received"));
1309 m.vis = syn::Visibility::Inherited;
1310 m.sig.ident = format_ident!("exit_received");
1311 if m.sig.asyncness.is_some() {
1312 has_async = true;
1313 }
1314 exit_method = Some(m);
1315 continue;
1316 }
1317
1318 if is_started && is_stopped {
1319 return syn::Error::new_spanned(
1320 &method.sig,
1321 "#[started] and #[stopped] cannot be combined on one method",
1322 )
1323 .to_compile_error()
1324 .into();
1325 }
1326
1327 if is_started {
1328 if started_method.is_some() {
1329 return syn::Error::new_spanned(
1330 &method.sig,
1331 "only one #[started] method is allowed per actor",
1332 )
1333 .to_compile_error()
1334 .into();
1335 }
1336 if method.attrs.iter().any(|a| {
1337 a.path().is_ident("handler")
1338 || a.path().is_ident("send_handler")
1339 || a.path().is_ident("request_handler")
1340 }) {
1341 return syn::Error::new_spanned(
1342 &method.sig,
1343 "#[started] cannot be combined with handler attributes",
1344 )
1345 .to_compile_error()
1346 .into();
1347 }
1348 if method.sig.inputs.len() != 2 {
1350 return syn::Error::new_spanned(
1351 &method.sig,
1352 "#[started] method must take exactly (&mut self, &Context<Self>)",
1353 )
1354 .to_compile_error()
1355 .into();
1356 }
1357 if !matches!(method.sig.inputs.first(), Some(FnArg::Receiver(r)) if r.reference.is_some() && r.mutability.is_some())
1358 {
1359 return syn::Error::new_spanned(
1360 &method.sig,
1361 "#[started] method's first parameter must be `&mut self`",
1362 )
1363 .to_compile_error()
1364 .into();
1365 }
1366 let mut m = method.clone();
1367 m.attrs.retain(|a| !a.path().is_ident("started"));
1368 m.vis = syn::Visibility::Inherited;
1369 m.sig.ident = format_ident!("started");
1370 if m.sig.asyncness.is_some() {
1371 has_async = true;
1372 }
1373 started_method = Some(m);
1374 continue;
1375 }
1376
1377 if is_stopped {
1378 if stopped_method.is_some() {
1379 return syn::Error::new_spanned(
1380 &method.sig,
1381 "only one #[stopped] method is allowed per actor",
1382 )
1383 .to_compile_error()
1384 .into();
1385 }
1386 if method.attrs.iter().any(|a| {
1387 a.path().is_ident("handler")
1388 || a.path().is_ident("send_handler")
1389 || a.path().is_ident("request_handler")
1390 }) {
1391 return syn::Error::new_spanned(
1392 &method.sig,
1393 "#[stopped] cannot be combined with handler attributes",
1394 )
1395 .to_compile_error()
1396 .into();
1397 }
1398 if method.sig.inputs.len() != 2 {
1400 return syn::Error::new_spanned(
1401 &method.sig,
1402 "#[stopped] method must take exactly (&mut self, &Context<Self>)",
1403 )
1404 .to_compile_error()
1405 .into();
1406 }
1407 if !matches!(method.sig.inputs.first(), Some(FnArg::Receiver(r)) if r.reference.is_some() && r.mutability.is_some())
1408 {
1409 return syn::Error::new_spanned(
1410 &method.sig,
1411 "#[stopped] method's first parameter must be `&mut self`",
1412 )
1413 .to_compile_error()
1414 .into();
1415 }
1416 let mut m = method.clone();
1417 m.attrs.retain(|a| !a.path().is_ident("stopped"));
1418 m.vis = syn::Visibility::Inherited;
1419 m.sig.ident = format_ident!("stopped");
1420 if m.sig.asyncness.is_some() {
1421 has_async = true;
1422 }
1423 stopped_method = Some(m);
1424 continue;
1425 }
1426 }
1427 items_to_keep.push(item);
1428 }
1429 impl_block.items = items_to_keep;
1430
1431 let mut handler_impls = Vec::new();
1433
1434 for item in &mut impl_block.items {
1435 if let ImplItem::Fn(method) = item {
1436 let handler_idx = method.attrs.iter().position(|attr| {
1437 attr.path().is_ident("handler")
1438 || attr.path().is_ident("send_handler")
1439 || attr.path().is_ident("request_handler")
1440 });
1441
1442 if let Some(idx) = handler_idx {
1443 method.attrs.remove(idx);
1444 if let Some(extra) = method.attrs.iter().find(|a| is_handler_attribute(a)) {
1445 return syn::Error::new_spanned(
1446 extra,
1447 "a method can carry only one of #[handler], #[send_handler] and #[request_handler]",
1448 )
1449 .to_compile_error()
1450 .into();
1451 }
1452 if !method.sig.generics.params.is_empty() {
1453 return syn::Error::new_spanned(
1454 &method.sig.generics,
1455 "handler methods cannot be generic; write one handler per message type",
1456 )
1457 .to_compile_error()
1458 .into();
1459 }
1460
1461 let extra_attrs: Vec<_> = method
1464 .attrs
1465 .iter()
1466 .filter(|a| {
1467 !a.path().is_ident("handler")
1468 && !a.path().is_ident("send_handler")
1469 && !a.path().is_ident("request_handler")
1470 && !a.path().is_ident("started")
1471 && !a.path().is_ident("stopped")
1472 })
1473 .cloned()
1474 .collect();
1475
1476 let method_name = &method.sig.ident;
1477 if method.sig.asyncness.is_some() {
1478 has_async = true;
1479 }
1480
1481 let param_count = method.sig.inputs.len();
1483 if param_count != 3 {
1484 return syn::Error::new_spanned(
1485 &method.sig,
1486 format!(
1487 "handler method must have 3 parameters (&mut self, msg: M, ctx: &Context<Self>), found {param_count}"
1488 ),
1489 )
1490 .to_compile_error()
1491 .into();
1492 }
1493
1494 match method.sig.inputs.first() {
1496 Some(FnArg::Receiver(r))
1497 if r.reference.is_some() || r.colon_token.is_some() => {}
1498 Some(first) => {
1499 return syn::Error::new_spanned(
1500 first,
1501 "handler method's first parameter must be `&mut self`",
1502 )
1503 .to_compile_error()
1504 .into();
1505 }
1506 None => unreachable!("three parameters were checked above"),
1507 }
1508 if let Some(FnArg::Typed(ctx)) = method.sig.inputs.iter().nth(2) {
1509 if !matches!(&*ctx.ty, Type::Reference(r) if r.mutability.is_none()) {
1510 return syn::Error::new_spanned(
1511 &ctx.ty,
1512 "handler context parameter must be `&Context<Self>`",
1513 )
1514 .to_compile_error()
1515 .into();
1516 }
1517 }
1518
1519 let msg_ty = match method.sig.inputs.iter().nth(1) {
1521 Some(FnArg::Typed(pat_type)) => &*pat_type.ty,
1522 _ => {
1523 return syn::Error::new_spanned(
1524 &method.sig,
1525 "handler method must have signature: fn(&mut self, msg: M, ctx: &Context<Self>) -> R",
1526 )
1527 .to_compile_error()
1528 .into();
1529 }
1530 };
1531
1532 let output = match &method.sig.output {
1535 ReturnType::Default => quote! {},
1536 ReturnType::Type(_, ty) => quote! { -> #ty },
1537 };
1538 let mut params = proc_macro2::Group::new(
1541 proc_macro2::Delimiter::Parenthesis,
1542 quote! { &mut self, msg: #msg_ty, ctx: &Context<Self> },
1543 );
1544 params.set_span(method.sig.paren_token.span.join());
1545 let span = method.sig.span();
1548
1549 let handler_impl = if method.sig.asyncness.is_some() {
1550 quote::quote_spanned! { span =>
1551 #(#extra_attrs)*
1552 impl #impl_generics Handler<#msg_ty> for #self_ty #where_clause {
1553 async fn handle #params #output {
1554 self.#method_name(msg, ctx).await
1555 }
1556 }
1557 }
1558 } else {
1559 quote::quote_spanned! { span =>
1560 #(#extra_attrs)*
1561 impl #impl_generics Handler<#msg_ty> for #self_ty #where_clause {
1562 fn handle #params #output {
1563 self.#method_name(msg, ctx)
1564 }
1565 }
1566 }
1567 };
1568
1569 handler_impls.push(handler_impl);
1570 }
1571 }
1572 }
1573
1574 let lifecycle_methods: Vec<&ImplItemFn> = [
1576 started_method.as_ref(),
1577 stopped_method.as_ref(),
1578 exit_method.as_ref(),
1579 ]
1580 .into_iter()
1581 .flatten()
1582 .collect();
1583
1584 let protocol_doc = if bridge_traits.is_empty() {
1585 quote! {}
1586 } else {
1587 let lines: Vec<String> = bridge_traits.iter().map(|t| format!("- [`{t}`]")).collect();
1588 let doc_body = format!(
1589 "# Protocol\n\n\
1590 When started, `ActorRef<{ty}>` implements:\n\n\
1591 {lines}\n\n\
1592 See the protocol trait docs for the full API.",
1593 ty = quote!(#self_ty),
1594 lines = lines.join("\n"),
1595 );
1596 quote! { #[doc = #doc_body] }
1597 };
1598
1599 let actor_impl = quote! {
1600 #protocol_doc
1601 impl #impl_generics Actor for #self_ty #where_clause {
1602 #(#lifecycle_methods)*
1603 }
1604 };
1605
1606 let runtime_path = if has_async {
1608 quote! { spawned_concurrency::tasks }
1609 } else {
1610 quote! { spawned_concurrency::threads }
1611 };
1612
1613 let bridge_asserts: Vec<_> = bridge_traits
1614 .iter()
1615 .map(|trait_name| {
1616 quote! {
1617 const _: () = {
1618 fn _assert_bridge<__T: #trait_name>() {}
1619 fn _check #impl_generics () #where_clause {
1620 _assert_bridge::<#runtime_path::ActorRef<#self_ty>>();
1621 }
1622 };
1623 }
1624 })
1625 .collect();
1626
1627 let output = quote! {
1628 #actor_impl
1629 #impl_block
1630 #(#handler_impls)*
1631 #(#bridge_asserts)*
1632 };
1633
1634 output.into()
1635}