Skip to main content

spawned_macros/
lib.rs

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
9// --- Helpers for #[protocol] ---
10
11fn 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            // Insert underscore before uppercase if:
17            // - not at start, AND
18            // - previous char is lowercase, OR next char is lowercase (handles acronyms)
19            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
51// A method-level cfg_attr has not been expanded when the enclosing protocol
52// macro runs. Reject conditional timeout attributes even for false predicates;
53// otherwise Rust sees an unknown attribute, or silently omits the timeout.
54fn 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                // Result<T, ActorError> where T ≠ () is no longer supported;
96                // use Response<T> which works in both modes.
97                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
132/// Protocol parameters become fields of a `'static` message struct. Report
133/// borrows and `impl Trait` here; rustc's own errors point into the expansion.
134fn 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        // fn pointers and trait objects may use higher-ranked borrows.
172        _ => 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
183/// Returns true for types available without explicit import (prelude + primitives).
184/// These can't be accessed via `super::` so must be left unqualified.
185fn is_prelude_or_primitive(name: &str) -> bool {
186    matches!(
187        name,
188        // Primitives
189        "bool" | "char" | "str"
190            | "i8" | "i16" | "i32" | "i64" | "i128" | "isize"
191            | "u8" | "u16" | "u32" | "u64" | "u128" | "usize"
192            | "f32" | "f64"
193            // Prelude types (Rust 2021)
194            | "Box" | "String" | "Vec"
195            | "Option" | "Some" | "None"
196            | "Result" | "Ok" | "Err"
197            | "ToString" | "ToOwned"
198    )
199}
200
201/// Qualify a type with `super::` so it resolves to the parent module's scope.
202/// This prevents name collisions when a generated struct name shadows an imported type
203/// via `use super::*`.
204///
205/// Prelude/primitive types are left unqualified (they can't be accessed via `super::`).
206/// User-defined types get `super::` prepended: `Event` → `super::Event`.
207/// Generic args are recursively qualified: `Vec<Event>` → `Vec<super::Event>`.
208fn qualify_type_with_super(ty: &Type) -> Type {
209    match ty {
210        Type::Path(TypePath { qself, path }) => {
211            // Leave qualified paths as-is: <X as T>::Y, ::abs::path, crate::, super::, self::
212            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            // Recursively qualify generic arguments in all segments
226            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            // Only prepend super:: for non-prelude types
243            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            // Prepend super:: for user-defined types
256            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
332/// Qualify a path's generic arguments with `super::`, used for trait bounds
333/// in `dyn Trait` and `impl Trait` types.
334fn 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
366/// One transport signature used in declarations, runtime impls and delegation.
367fn 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
390/// Runtime-specific transport primitives, shared by plain and configured handles.
391/// Cfg-gated Handler bounds are represented by one marker set per runtime.
392fn 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    // Unconditional methods: Handler bounds go directly on the impl block.
406    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    // Group cfg-gated methods by their cfg predicate.
416    // Each unique group gets a marker trait that conditionally requires the Handler bounds.
417    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        // Combine cfg predicates and negate for the fallback impl.
452        // #[cfg(A)] #[cfg(B)] -> active: all(A, B), inactive: not(all(A, B))
453        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
508/// Share transport bodies and one method template while keeping public protocol
509/// impls concrete. Supertraits constrain each nominal target independently.
510fn 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                    // Evaluate before message construction can move arguments;
567                    // an explicit override does not evaluate the default expression.
568                    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    // Keep public impls nominal. A blanket impl over the hidden target trait
582    // conflicts with existing Box<T>/&T protocol forwarding implementations.
583    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    /// Doc + cfg attributes to propagate to blanket impl methods.
646    method_attrs: Vec<Attribute>,
647    cfg_attrs: Vec<Attribute>,
648    timeout: syn::Expr,
649}
650
651/// Generates message types and blanket implementations from a protocol trait.
652///
653/// `#[protocol]` transforms a trait definition into a full message-passing interface.
654/// Each trait method becomes a message struct, and any `ActorRef<A>` where `A` handles
655/// those messages automatically implements the trait — so you call methods directly on
656/// the actor reference.
657///
658/// # What It Generates
659///
660/// For a trait `FooProtocol`, the macro generates:
661///
662/// 1. **Message structs** in a `foo_protocol` submodule — one per method, with public
663///    fields matching the method parameters. Each implements `Message`.
664///    Unit structs (methods with no parameters beyond `&self`) automatically derive `Clone`.
665/// 2. **Type alias** `pub type FooRef = Arc<dyn FooProtocol>` — a type-erased reference.
666/// 3. **Converter trait** `ToFooRef` with a `to_foo_ref(&self) -> FooRef` method.
667/// 4. **Blanket impls** — `impl FooProtocol for ActorRef<A>` and `impl ToFooRef for ActorRef<A>`
668///    for any actor `A` that handles all the generated message types.
669///
670/// # Type Resolution in Generated Structs
671///
672/// The generated message module uses `use super::*` to access types from the
673/// parent scope. Types used in method parameters are qualified with `super::`
674/// in the generated structs — this means any type you reference in a protocol
675/// method signature must be in scope where the `#[protocol]` trait is defined.
676///
677/// Prelude types (`String`, `Vec`, `Option`, `Result`, `Box`, `bool`, `u32`, etc.)
678/// and fully qualified paths (`std::`, `core::`, `alloc::`) are used as-is without
679/// `super::` prefixing.
680///
681/// # Return Type Conventions
682///
683/// The return type on each method determines the message kind:
684///
685/// | Return type | Kind | Runtime | Caller behavior |
686/// |-------------|------|---------|-----------------|
687/// | `Response<T>` | Request | Both | `.await.unwrap()` (tasks) / `.unwrap()` (threads) — 5s default timeout |
688/// | `Result<(), ActorError>` | Send | Both | Returns send result |
689/// | *(none)* / `-> ()` | Send | Both | Fire-and-forget (discards send result) |
690///
691/// # Naming
692///
693/// - **Module**: trait name → `snake_case` (e.g., `ChatRoomProtocol` → `chat_room_protocol`)
694/// - **Structs**: method name → `PascalCase` (e.g., `send_message` → `SendMessage`)
695/// - **XRef alias**: strips `Protocol` suffix → `{Base}Ref` (e.g., `ChatRoomProtocol` → `ChatRoomRef`)
696/// - **Converter**: `To{Base}Ref` with `to_{snake_base}_ref()` method
697///
698/// # Example
699///
700/// ```ignore
701/// use spawned_concurrency::Response;
702/// use spawned_concurrency::protocol;
703///
704/// #[protocol]
705/// pub trait CounterProtocol: Send + Sync {
706///     fn increment(&self, amount: u64);                     // send (fire-and-forget)
707///     fn get_count(&self) -> Response<u64>;                 // request (both modes)
708/// }
709///
710/// // Generated:
711/// // - pub mod counter_protocol { pub struct Increment { pub amount: u64 }, pub struct GetCount }
712/// // - pub type CounterRef = Arc<dyn CounterProtocol>;
713/// // - pub trait ToCounterRef { fn to_counter_ref(&self) -> CounterRef; }
714/// // - impl CounterProtocol for ActorRef<A> where A: Handler<Increment> + Handler<GetCount>
715/// ```
716/// # Request timeouts
717///
718/// `#[protocol(timeout = Duration::from_secs(30))]` sets a protocol default;
719/// `#[request_timeout(Duration::from_secs(2))]` overrides a request method.
720/// Without either setting, requests keep their five-second default.
721/// `actor.with_request_timeout(limit).method(...)` takes precedence and also
722/// supports conversion with the generated `ToXRef` trait. Configure before
723/// erasing the concrete handle. These are reply-wait limits, not cancellation
724/// or inherited deadlines; accepted work may still execute after timeout.
725/// Native protocol calls block immediately; async response timers start on poll.
726#[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            // Verify first param is &self
799            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            // cfg-gated methods may legitimately share a name across exclusive
847            // predicates; only unconditional collisions are reported here.
848            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    // Generate message structs
975    // Field types and result types are qualified with `super::` to prevent
976    // name collisions when a generated struct name shadows an imported type.
977    // e.g., `fn event(&self, event: Event)` generates `struct Event { pub event: super::Event }`
978    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    // Gate async impls using the dependency feature namespace, not the consumer.
1018    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    // Protocol traits are consumed via blanket impls and dyn references,
1044    // so rustc may report methods as unused during development.
1045    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/// Generates `impl Actor` and `Handler<M>` implementations from an annotated impl block.
1088///
1089/// Place `#[actor]` on an `impl MyStruct` block. The macro extracts annotated methods
1090/// and generates the boilerplate needed to run the struct as an actor.
1091///
1092/// # Protocol Assertion
1093///
1094/// Use `protocol = TraitName` to verify at compile time that the actor fully implements
1095/// a protocol (i.e., that `ActorRef<Self>` implements the protocol trait):
1096///
1097/// ```ignore
1098/// #[actor(protocol = NameServerProtocol)]
1099/// impl NameServer { /* ... */ }
1100/// ```
1101///
1102/// The runtime (tasks vs threads) is inferred from whether any handler in the block
1103/// is `async fn`. If all handlers are defined outside the `#[actor]` block, put at
1104/// least one handler inside so the macro can detect the correct runtime.
1105///
1106/// For multiple protocols:
1107/// ```ignore
1108/// #[actor(protocol(RoomProtocol, UserProtocol))]
1109/// impl ChatUser { /* ... */ }
1110/// ```
1111///
1112/// # Handler Attributes
1113///
1114/// | Attribute | Use for |
1115/// |-----------|---------|
1116/// | `#[request_handler]` | Messages that expect a reply (`Response<T>`) |
1117/// | `#[send_handler]` | Fire-and-forget messages |
1118/// | `#[handler]` | Generic handler (works for either kind) |
1119///
1120/// Handler signature: `fn name(&mut self, msg: MessageType, ctx: &Context<Self>) -> ReturnType`
1121///
1122/// The return type must match the message's `Message::Result`. For request handlers
1123/// returning `()`, omit the return type.
1124///
1125/// # Lifecycle Hooks
1126///
1127/// | Attribute | Called |
1128/// |-----------|--------|
1129/// | `#[started]` | After the actor starts, before processing messages |
1130/// | `#[stopped]` | After the actor stops processing messages |
1131/// | `#[exit_received]` | A linked exit delivered while trapping exits |
1132///
1133/// Both receive `&mut self` and `&Context<Self>`. Use async or sync to match your runtime.
1134///
1135/// # Example
1136///
1137/// ```ignore
1138/// use spawned_concurrency::tasks::{Actor, Context, Handler};
1139/// use spawned_concurrency::actor;
1140///
1141/// pub struct MyActor { count: u64 }
1142///
1143/// #[actor(protocol = CounterProtocol)]
1144/// impl MyActor {
1145///     pub fn new() -> Self { MyActor { count: 0 } }
1146///
1147///     #[started]
1148///     async fn on_start(&mut self, _ctx: &Context<Self>) {
1149///         tracing::info!("Actor started");
1150///     }
1151///
1152///     #[send_handler]
1153///     async fn handle_increment(&mut self, msg: Increment, _ctx: &Context<Self>) {
1154///         self.count += msg.amount;
1155///     }
1156///
1157///     #[request_handler]
1158///     async fn handle_get_count(&mut self, _msg: GetCount, _ctx: &Context<Self>) -> u64 {
1159///         self.count
1160///     }
1161/// }
1162/// ```
1163#[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    // Tasks actors use `async fn` for every handler and hook, threads actors
1178    // plain `fn`; the runtime is inferred from them. A sync method returning
1179    // `impl Future` is valid in tasks mode, and cfg-gated methods may differ.
1180    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    // --- Parse named parameters from #[actor(protocol = X)] or #[actor(protocol(X, Y))] ---
1207    let bridge_traits: Vec<Ident> = if attr.is_empty() {
1208        Vec::new()
1209    } else {
1210        // A protocol is named by a single identifier; report paths explicitly
1211        // instead of failing on the `::` token.
1212        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                    // protocol = TraitName
1234                    let _: syn::Token![=] = input.parse()?;
1235                    protocols.push(protocol_name(input)?);
1236                } else if input.peek(syn::token::Paren) {
1237                    // protocol(Trait1, Trait2)
1238                    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    // --- Extract #[started] and #[stopped] lifecycle methods ---
1263    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                // Expect: fn started(&mut self, ctx: &Context<Self>)
1349                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                // Expect: fn stopped(&mut self, ctx: &Context<Self>)
1399                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    // --- Process handler methods ---
1432    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                // Collect remaining attributes (e.g. #[cfg(...)]) to propagate
1462                // to the generated Handler impl block.
1463                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                // Validate handler has exactly 3 parameters: &mut self, msg, ctx
1482                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                // `&self` is accepted too; a missing or by-value receiver is not.
1495                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                // Extract message type from 2nd parameter (index 1, after &mut self)
1520                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                // Keep an omitted return type omitted: rustc then suggests
1533                // adding `-> R` after the user's signature for a request reply.
1534                let output = match &method.sig.output {
1535                    ReturnType::Default => quote! {},
1536                    ReturnType::Type(_, ty) => quote! { -> #ty },
1537                };
1538                // The parameter list carries the user's parentheses span, so
1539                // that suggestion lands after the user's `)`.
1540                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                // Span the generated impl at the method so trait errors
1546                // (conflicting impls, signature mismatches) point at it.
1547                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    // --- Generate impl Actor ---
1575    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    // --- Generate bridge assertions ---
1607    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}