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::Parse, parse_macro_input, 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
132fn is_unit_type(ty: &Type) -> bool {
133    if let Type::Tuple(tuple) = ty {
134        return tuple.elems.is_empty();
135    }
136    false
137}
138
139/// Returns true for types available without explicit import (prelude + primitives).
140/// These can't be accessed via `super::` so must be left unqualified.
141fn is_prelude_or_primitive(name: &str) -> bool {
142    matches!(
143        name,
144        // Primitives
145        "bool" | "char" | "str"
146            | "i8" | "i16" | "i32" | "i64" | "i128" | "isize"
147            | "u8" | "u16" | "u32" | "u64" | "u128" | "usize"
148            | "f32" | "f64"
149            // Prelude types (Rust 2021)
150            | "Box" | "String" | "Vec"
151            | "Option" | "Some" | "None"
152            | "Result" | "Ok" | "Err"
153            | "ToString" | "ToOwned"
154    )
155}
156
157/// Qualify a type with `super::` so it resolves to the parent module's scope.
158/// This prevents name collisions when a generated struct name shadows an imported type
159/// via `use super::*`.
160///
161/// Prelude/primitive types are left unqualified (they can't be accessed via `super::`).
162/// User-defined types get `super::` prepended: `Event` → `super::Event`.
163/// Generic args are recursively qualified: `Vec<Event>` → `Vec<super::Event>`.
164fn qualify_type_with_super(ty: &Type) -> Type {
165    match ty {
166        Type::Path(TypePath { qself, path }) => {
167            // Leave qualified paths as-is: <X as T>::Y, ::abs::path, crate::, super::, self::
168            if qself.is_some() || path.leading_colon.is_some() {
169                return ty.clone();
170            }
171            if let Some(first) = path.segments.first() {
172                let s = first.ident.to_string();
173                if matches!(
174                    s.as_str(),
175                    "crate" | "super" | "self" | "std" | "core" | "alloc"
176                ) {
177                    return ty.clone();
178                }
179            }
180
181            // Recursively qualify generic arguments in all segments
182            let qualified_segments: syn::punctuated::Punctuated<_, _> = path
183                .segments
184                .iter()
185                .map(|seg| {
186                    let mut new_seg = seg.clone();
187                    if let PathArguments::AngleBracketed(ref mut args) = new_seg.arguments {
188                        for arg in &mut args.args {
189                            if let GenericArgument::Type(ref mut inner) = arg {
190                                *inner = qualify_type_with_super(inner);
191                            }
192                        }
193                    }
194                    new_seg
195                })
196                .collect();
197
198            // Only prepend super:: for non-prelude types
199            if let Some(first) = path.segments.first() {
200                if is_prelude_or_primitive(&first.ident.to_string()) {
201                    return Type::Path(TypePath {
202                        qself: None,
203                        path: syn::Path {
204                            leading_colon: None,
205                            segments: qualified_segments,
206                        },
207                    });
208                }
209            }
210
211            // Prepend super:: for user-defined types
212            let mut segments = syn::punctuated::Punctuated::new();
213            segments.push(syn::PathSegment {
214                ident: format_ident!("super"),
215                arguments: PathArguments::None,
216            });
217            for seg in qualified_segments {
218                segments.push(seg);
219            }
220
221            Type::Path(TypePath {
222                qself: None,
223                path: syn::Path {
224                    leading_colon: None,
225                    segments,
226                },
227            })
228        }
229        Type::Reference(r) => {
230            let mut new = r.clone();
231            new.elem = Box::new(qualify_type_with_super(&r.elem));
232            Type::Reference(new)
233        }
234        Type::Tuple(t) => {
235            let mut new = t.clone();
236            for elem in &mut new.elems {
237                *elem = qualify_type_with_super(elem);
238            }
239            Type::Tuple(new)
240        }
241        Type::Array(a) => {
242            let mut new = a.clone();
243            *new.elem = qualify_type_with_super(&a.elem);
244            Type::Array(new)
245        }
246        Type::Slice(s) => {
247            let mut new = s.clone();
248            *new.elem = qualify_type_with_super(&s.elem);
249            Type::Slice(new)
250        }
251        Type::Paren(p) => {
252            let mut new = p.clone();
253            *new.elem = qualify_type_with_super(&p.elem);
254            Type::Paren(new)
255        }
256        Type::TraitObject(t) => {
257            let mut new = t.clone();
258            for bound in &mut new.bounds {
259                if let syn::TypeParamBound::Trait(tb) = bound {
260                    qualify_path_with_super(&mut tb.path);
261                }
262            }
263            Type::TraitObject(new)
264        }
265        Type::ImplTrait(t) => {
266            let mut new = t.clone();
267            for bound in &mut new.bounds {
268                if let syn::TypeParamBound::Trait(tb) = bound {
269                    qualify_path_with_super(&mut tb.path);
270                }
271            }
272            Type::ImplTrait(new)
273        }
274        Type::BareFn(f) => {
275            let mut new = f.clone();
276            for arg in &mut new.inputs {
277                arg.ty = qualify_type_with_super(&arg.ty);
278            }
279            if let ReturnType::Type(_, ref mut ty) = new.output {
280                **ty = qualify_type_with_super(ty);
281            }
282            Type::BareFn(new)
283        }
284        _ => ty.clone(),
285    }
286}
287
288/// Qualify a path's generic arguments with `super::`, used for trait bounds
289/// in `dyn Trait` and `impl Trait` types.
290fn qualify_path_with_super(path: &mut syn::Path) {
291    for seg in &mut path.segments {
292        match &mut seg.arguments {
293            PathArguments::AngleBracketed(ref mut args) => {
294                for arg in &mut args.args {
295                    if let GenericArgument::Type(ref mut inner) = arg {
296                        *inner = qualify_type_with_super(inner);
297                    }
298                }
299            }
300            PathArguments::Parenthesized(ref mut args) => {
301                for input in &mut args.inputs {
302                    *input = qualify_type_with_super(input);
303                }
304                if let syn::ReturnType::Type(_, ref mut ty) = args.output {
305                    **ty = qualify_type_with_super(ty);
306                }
307            }
308            PathArguments::None => {}
309        }
310    }
311}
312
313struct ProtocolInfo<'a> {
314    trait_name: &'a Ident,
315    mod_name: &'a Ident,
316    ref_name: &'a Ident,
317    converter_trait: &'a Ident,
318    converter_method: &'a Ident,
319    target_trait: &'a Ident,
320}
321
322/// One transport signature used in declarations, runtime impls and delegation.
323fn transport_signature(
324    info: &ProtocolInfo,
325    method: &ProtocolMethodInfo,
326    index: usize,
327) -> (Ident, proc_macro2::TokenStream) {
328    let name = format_ident!("__dispatch_{}", index);
329    let module = info.mod_name;
330    let message = &method.struct_name;
331    let (timeout, result) = match &method.kind {
332        MethodKind::Send => (
333            quote! {},
334            quote! { ::std::result::Result<(), spawned_concurrency::ActorError> },
335        ),
336        MethodKind::Request(result) => (
337            quote! { , timeout: ::std::time::Duration },
338            quote! { spawned_concurrency::Response<#result> },
339        ),
340    };
341    let signature =
342        quote! { fn #name(target: &Self, message: #module::#message #timeout) -> #result };
343    (name, signature)
344}
345
346/// Runtime-specific transport primitives, shared by plain and configured handles.
347/// Cfg-gated Handler bounds are represented by one marker set per runtime.
348fn generate_target_impl(
349    info: &ProtocolInfo,
350    methods: &[ProtocolMethodInfo],
351    runtime_path: &proc_macro2::TokenStream,
352    mode: RuntimeMode,
353) -> proc_macro2::TokenStream {
354    let ProtocolInfo {
355        trait_name,
356        mod_name,
357        target_trait,
358        ..
359    } = info;
360
361    // Unconditional methods: Handler bounds go directly on the impl block.
362    let handler_bounds: Vec<_> = methods
363        .iter()
364        .filter(|m| m.cfg_attrs.is_empty())
365        .map(|m| {
366            let sn = &m.struct_name;
367            quote! { #runtime_path::Handler<#mod_name::#sn> }
368        })
369        .collect();
370
371    // Group cfg-gated methods by their cfg predicate.
372    // Each unique group gets a marker trait that conditionally requires the Handler bounds.
373    let mut cfg_groups: Vec<(String, Vec<&ProtocolMethodInfo>)> = Vec::new();
374    for m in methods.iter().filter(|m| !m.cfg_attrs.is_empty()) {
375        let key: String = m
376            .cfg_attrs
377            .iter()
378            .map(|a| quote!(#a).to_string())
379            .collect::<Vec<_>>()
380            .join(",");
381        if let Some(group) = cfg_groups.iter_mut().find(|(k, _)| k == &key) {
382            group.1.push(m);
383        } else {
384            cfg_groups.push((key, vec![m]));
385        }
386    }
387
388    let mode_suffix = match mode {
389        RuntimeMode::Tasks => format_ident!("Tasks"),
390        RuntimeMode::Threads => format_ident!("Threads"),
391    };
392
393    let mut marker_trait_defs = Vec::new();
394    let mut marker_trait_bounds = Vec::new();
395
396    for (i, (_key, group_methods)) in cfg_groups.iter().enumerate() {
397        let marker_name = format_ident!("__{}Cfg{}{}", trait_name, i, mode_suffix);
398        let cfg_attrs = &group_methods[0].cfg_attrs;
399        let group_handler_bounds: Vec<_> = group_methods
400            .iter()
401            .map(|m| {
402                let sn = &m.struct_name;
403                quote! { #runtime_path::Handler<#mod_name::#sn> }
404            })
405            .collect();
406
407        // Combine cfg predicates and negate for the fallback impl.
408        // #[cfg(A)] #[cfg(B)] -> active: all(A, B), inactive: not(all(A, B))
409        let cfg_predicates: Vec<proc_macro2::TokenStream> = cfg_attrs
410            .iter()
411            .filter(|a| a.path().is_ident("cfg"))
412            .filter_map(|a| a.parse_args::<proc_macro2::TokenStream>().ok())
413            .collect();
414
415        let (positive_cfg, negated_cfg) = if cfg_predicates.len() == 1 {
416            let pred = &cfg_predicates[0];
417            (quote! { #[cfg(#pred)] }, quote! { #[cfg(not(#pred))] })
418        } else {
419            (
420                quote! { #[cfg(all(#(#cfg_predicates),*))] },
421                quote! { #[cfg(not(all(#(#cfg_predicates),*)))] },
422            )
423        };
424
425        marker_trait_defs.push(quote! {
426            #positive_cfg
427            #[doc(hidden)]
428            trait #marker_name: #(#group_handler_bounds)+* {}
429            #positive_cfg
430            impl<__T: #(#group_handler_bounds)+*> #marker_name for __T {}
431
432            #negated_cfg
433            #[doc(hidden)]
434            trait #marker_name {}
435            #negated_cfg
436            impl<__T> #marker_name for __T {}
437        });
438
439        marker_trait_bounds.push(quote! { + #marker_name });
440    }
441
442    let method_impls = methods.iter().enumerate().map(|(index, m)| {
443        let (_, signature) = transport_signature(info, m, index);
444        let cfg = &m.cfg_attrs;
445        let body = match &m.kind {
446            MethodKind::Send => quote! { target.send(message) },
447            MethodKind::Request(_) => match mode {
448                RuntimeMode::Tasks => quote! { spawned_concurrency::Response::from_with_timeout(target.request_raw(message), timeout) },
449                RuntimeMode::Threads => quote! { spawned_concurrency::Response::ready(target.request_with_timeout(message, timeout)) },
450            },
451        };
452        quote! { #(#cfg)* #signature { #body } }
453    });
454    quote! {
455        #(#marker_trait_defs)*
456        impl<__A: #runtime_path::Actor #(+ #handler_bounds)* #(#marker_trait_bounds)*>
457            #target_trait for #runtime_path::ActorRef<__A>
458        {
459            #(#method_impls)*
460        }
461    }
462}
463
464/// Share transport bodies and one method template while keeping public protocol
465/// impls concrete. Supertraits constrain each nominal target independently.
466fn generate_protocol_adapters(
467    info: &ProtocolInfo,
468    methods: &[ProtocolMethodInfo],
469    definition: &ItemTrait,
470) -> proc_macro2::TokenStream {
471    let ProtocolInfo {
472        trait_name,
473        mod_name,
474        ref_name,
475        converter_trait,
476        converter_method,
477        target_trait,
478    } = info;
479    let signatures = methods.iter().enumerate().map(|(index, m)| {
480        let (_, signature) = transport_signature(info, m, index);
481        let cfg = &m.cfg_attrs;
482        quote! { #(#cfg)* #signature; }
483    });
484    let delegates = methods.iter().enumerate().map(|(index, m)| {
485        let (name, signature) = transport_signature(info, m, index);
486        let cfg = &m.cfg_attrs;
487        let timeout = match &m.kind { MethodKind::Send => quote! {}, MethodKind::Request(_) => quote! { , timeout } };
488        quote! {
489            #(#cfg)* #signature {
490                <__T as #target_trait>::#name(
491                    spawned_concurrency::response::RequestOptions::actor_ref(target), message #timeout
492                )
493            }
494        }
495    });
496    let method_impls: Vec<_> = methods.iter().enumerate().map(|(index, m)| {
497        let method = &m.method_name;
498        let dispatch = format_ident!("__dispatch_{}", index);
499        let params = &m.params;
500        let attrs = &m.method_attrs;
501        let result = &m.ret_type;
502        let message = &m.struct_name;
503        let fields = &m.field_names;
504        let construct = if fields.is_empty() {
505            quote! { #mod_name::#message }
506        } else {
507            quote! { #mod_name::#message { #(#fields),* } }
508        };
509        let body = match &m.kind {
510            MethodKind::Send => {
511                let call = quote! { <Self as #target_trait>::#dispatch(self, #construct) };
512                let unit = match result {
513                    ReturnType::Default => true,
514                    ReturnType::Type(_, ty) => is_unit_type(ty),
515                };
516                if unit { quote! { let _ = #call; } } else { call }
517            }
518            MethodKind::Request(_) => {
519                let timeout = &m.timeout;
520                let binding = syn::Ident::new("__spawned_request_timeout", proc_macro2::Span::mixed_site());
521                quote! {
522                    // Evaluate before message construction can move arguments;
523                    // an explicit override does not evaluate the default expression.
524                    let #binding: ::std::time::Duration = match <Self as #target_trait>::__timeout_override(self) {
525                        Some(timeout) => timeout,
526                        None => #timeout,
527                    };
528                    <Self as #target_trait>::#dispatch(self, #construct, #binding)
529                }
530            }
531        };
532        quote! {
533            #(#attrs)*
534            fn #method(&self, #(#params),*) #result { #body }
535        }
536    }).collect();
537    // Keep public impls nominal. A blanket impl over the hidden target trait
538    // conflicts with existing Box<T>/&T protocol forwarding implementations.
539    let mut public_impls = Vec::new();
540    for (runtime, asynchronous) in [
541        (quote! { spawned_concurrency::tasks }, true),
542        (quote! { spawned_concurrency::threads }, false),
543    ] {
544        let actor = quote! { #runtime::ActorRef<__A> };
545        for target in [
546            actor.clone(),
547            quote! { spawned_concurrency::response::RequestOptions<#actor> },
548        ] {
549            let mut predicates: Vec<_> = definition
550                .generics
551                .where_clause
552                .iter()
553                .flat_map(|clause| clause.predicates.iter())
554                .map(|p| quote! { #p })
555                .collect();
556            let supertraits = &definition.supertraits;
557            if !supertraits.is_empty() {
558                predicates.push(quote! { Self: #supertraits });
559            }
560            let implementation = quote! {
561                impl<__A: #runtime::Actor> #trait_name for #target
562                    where Self: #target_trait, #(#predicates,)*
563                { #(#method_impls)* }
564                impl<__A: #runtime::Actor> #converter_trait for #target
565                    where Self: #target_trait + #trait_name
566                {
567                    fn #converter_method(&self) -> #ref_name { ::std::sync::Arc::new(self.clone()) }
568                }
569            };
570            public_impls.push(if asynchronous {
571                quote! { spawned_concurrency::__spawned_async_items! { #implementation } }
572            } else {
573                implementation
574            });
575        }
576    }
577    quote! {
578        #[doc(hidden)]
579        trait #target_trait: Clone + Send + Sync + 'static {
580            fn __timeout_override(_target: &Self) -> Option<::std::time::Duration> { None }
581            #(#signatures)*
582        }
583        impl<__T: #target_trait> #target_trait for spawned_concurrency::response::RequestOptions<__T> {
584            fn __timeout_override(target: &Self) -> Option<::std::time::Duration> {
585                Some(spawned_concurrency::response::RequestOptions::timeout(target))
586            }
587            #(#delegates)*
588        }
589        #(#public_impls)*
590    }
591}
592
593struct ProtocolMethodInfo {
594    method_name: Ident,
595    struct_name: Ident,
596    field_names: Vec<Ident>,
597    field_types: Vec<Type>,
598    kind: MethodKind,
599    params: Vec<FnArg>,
600    ret_type: ReturnType,
601    /// Doc + cfg attributes to propagate to blanket impl methods.
602    method_attrs: Vec<Attribute>,
603    cfg_attrs: Vec<Attribute>,
604    timeout: syn::Expr,
605}
606
607/// Generates message types and blanket implementations from a protocol trait.
608///
609/// `#[protocol]` transforms a trait definition into a full message-passing interface.
610/// Each trait method becomes a message struct, and any `ActorRef<A>` where `A` handles
611/// those messages automatically implements the trait — so you call methods directly on
612/// the actor reference.
613///
614/// # What It Generates
615///
616/// For a trait `FooProtocol`, the macro generates:
617///
618/// 1. **Message structs** in a `foo_protocol` submodule — one per method, with public
619///    fields matching the method parameters. Each implements `Message`.
620///    Unit structs (methods with no parameters beyond `&self`) automatically derive `Clone`.
621/// 2. **Type alias** `pub type FooRef = Arc<dyn FooProtocol>` — a type-erased reference.
622/// 3. **Converter trait** `ToFooRef` with a `to_foo_ref(&self) -> FooRef` method.
623/// 4. **Blanket impls** — `impl FooProtocol for ActorRef<A>` and `impl ToFooRef for ActorRef<A>`
624///    for any actor `A` that handles all the generated message types.
625///
626/// # Type Resolution in Generated Structs
627///
628/// The generated message module uses `use super::*` to access types from the
629/// parent scope. Types used in method parameters are qualified with `super::`
630/// in the generated structs — this means any type you reference in a protocol
631/// method signature must be in scope where the `#[protocol]` trait is defined.
632///
633/// Prelude types (`String`, `Vec`, `Option`, `Result`, `Box`, `bool`, `u32`, etc.)
634/// and fully qualified paths (`std::`, `core::`, `alloc::`) are used as-is without
635/// `super::` prefixing.
636///
637/// # Return Type Conventions
638///
639/// The return type on each method determines the message kind:
640///
641/// | Return type | Kind | Runtime | Caller behavior |
642/// |-------------|------|---------|-----------------|
643/// | `Response<T>` | Request | Both | `.await.unwrap()` (tasks) / `.unwrap()` (threads) — 5s default timeout |
644/// | `Result<(), ActorError>` | Send | Both | Returns send result |
645/// | *(none)* / `-> ()` | Send | Both | Fire-and-forget (discards send result) |
646///
647/// # Naming
648///
649/// - **Module**: trait name → `snake_case` (e.g., `ChatRoomProtocol` → `chat_room_protocol`)
650/// - **Structs**: method name → `PascalCase` (e.g., `send_message` → `SendMessage`)
651/// - **XRef alias**: strips `Protocol` suffix → `{Base}Ref` (e.g., `ChatRoomProtocol` → `ChatRoomRef`)
652/// - **Converter**: `To{Base}Ref` with `to_{snake_base}_ref()` method
653///
654/// # Example
655///
656/// ```ignore
657/// use spawned_concurrency::Response;
658/// use spawned_concurrency::protocol;
659///
660/// #[protocol]
661/// pub trait CounterProtocol: Send + Sync {
662///     fn increment(&self, amount: u64);                     // send (fire-and-forget)
663///     fn get_count(&self) -> Response<u64>;                 // request (both modes)
664/// }
665///
666/// // Generated:
667/// // - pub mod counter_protocol { pub struct Increment { pub amount: u64 }, pub struct GetCount }
668/// // - pub type CounterRef = Arc<dyn CounterProtocol>;
669/// // - pub trait ToCounterRef { fn to_counter_ref(&self) -> CounterRef; }
670/// // - impl CounterProtocol for ActorRef<A> where A: Handler<Increment> + Handler<GetCount>
671/// ```
672/// # Request timeouts
673///
674/// `#[protocol(timeout = Duration::from_secs(30))]` sets a protocol default;
675/// `#[request_timeout(Duration::from_secs(2))]` overrides a request method.
676/// Without either setting, requests keep their five-second default.
677/// `actor.with_request_timeout(limit).method(...)` takes precedence and also
678/// supports conversion with the generated `ToXRef` trait. Configure before
679/// erasing the concrete handle. These are reply-wait limits, not cancellation
680/// or inherited deadlines; accepted work may still execute after timeout.
681/// Native protocol calls block immediately; async response timers start on poll.
682#[proc_macro_attribute]
683pub fn protocol(attr: TokenStream, item: TokenStream) -> TokenStream {
684    let options = parse_macro_input!(attr with syn::punctuated::Punctuated::<syn::MetaNameValue, syn::Token![,]>::parse_terminated);
685    let mut protocol_timeout = None;
686    for option in options {
687        if !option.path.is_ident("timeout") {
688            return syn::Error::new_spanned(
689                option.path,
690                "unsupported protocol option; expected timeout = Duration expression",
691            )
692            .to_compile_error()
693            .into();
694        }
695        if protocol_timeout.replace(option.value).is_some() {
696            return syn::Error::new_spanned(option.path, "duplicate protocol timeout")
697                .to_compile_error()
698                .into();
699        }
700    }
701    let mut trait_def = parse_macro_input!(item as ItemTrait);
702    let trait_name = &trait_def.ident;
703    let trait_vis = &trait_def.vis;
704
705    if !trait_def.generics.params.is_empty() {
706        return syn::Error::new_spanned(
707            &trait_def.generics,
708            "generic type parameters on protocol traits are not supported",
709        )
710        .to_compile_error()
711        .into();
712    }
713
714    let base_name = strip_protocol_suffix(&trait_name.to_string());
715    let mod_name = format_ident!("{}", to_snake_case(&trait_name.to_string()));
716    let ref_name = format_ident!("{}Ref", base_name);
717    let converter_trait = format_ident!("To{}Ref", base_name);
718    let converter_method = format_ident!("to_{}_ref", to_snake_case(&base_name));
719
720    let mut methods: Vec<ProtocolMethodInfo> = Vec::new();
721
722    for item in &mut trait_def.items {
723        if !matches!(item, TraitItem::Fn(_)) {
724            return syn::Error::new_spanned(
725                item,
726                "protocol traits may only contain methods; \
727                 associated types, constants, and other items are not supported",
728            )
729            .to_compile_error()
730            .into();
731        }
732        if let TraitItem::Fn(method) = item {
733            if let Some(attribute) = method.attrs.iter().find(|a| conditional_timeout(&a.meta)) {
734                return syn::Error::new_spanned(
735                    attribute,
736                    "conditional request_timeout is not supported; use #[request_timeout(...)] directly",
737                ).to_compile_error().into();
738            }
739            if method.sig.asyncness.is_some() {
740                return syn::Error::new_spanned(
741                    &method.sig,
742                    "protocol methods must not be async; \
743                     use Response<T> as the return type for requests",
744                )
745                .to_compile_error()
746                .into();
747            }
748
749            // Verify first param is &self
750            match method.sig.inputs.first() {
751                Some(FnArg::Receiver(r)) if r.reference.is_some() && r.mutability.is_none() => {}
752                _ => {
753                    return syn::Error::new_spanned(
754                        &method.sig,
755                        "protocol methods must take `&self` as the first parameter",
756                    )
757                    .to_compile_error()
758                    .into();
759                }
760            }
761
762            let method_name = method.sig.ident.clone();
763            let struct_name = format_ident!("{}", to_pascal_case(&method_name.to_string()));
764
765            let mut field_names: Vec<Ident> = Vec::new();
766            let mut field_types: Vec<Type> = Vec::new();
767            let mut params: Vec<FnArg> = Vec::new();
768
769            for arg in method.sig.inputs.iter().skip(1) {
770                if let FnArg::Typed(pat_type) = arg {
771                    if let Pat::Ident(pat_ident) = &*pat_type.pat {
772                        field_names.push(pat_ident.ident.clone());
773                        field_types.push((*pat_type.ty).clone());
774                    } else {
775                        return syn::Error::new_spanned(
776                            &pat_type.pat,
777                            "protocol methods only support simple identifier patterns \
778                             (e.g., `name: Type`)",
779                        )
780                        .to_compile_error()
781                        .into();
782                    }
783                }
784                params.push(arg.clone());
785            }
786
787            let kind = match classify_return_type(&method.sig.output) {
788                Ok(kind) => kind,
789                Err(ty) => {
790                    return syn::Error::new_spanned(
791                        ty,
792                        "unsupported return type in protocol method; \
793                         use Response<T> for requests (works in both async and sync modes), \
794                         Result<(), ActorError> for sends, or no return type for fire-and-forget",
795                    )
796                    .to_compile_error()
797                    .into();
798                }
799            };
800
801            let mut method_timeout = None;
802            for attr in method
803                .attrs
804                .iter()
805                .filter(|a| a.path().is_ident("request_timeout"))
806            {
807                if !matches!(kind, MethodKind::Request(_)) {
808                    return syn::Error::new_spanned(
809                        attr,
810                        "request_timeout is only valid on Response<T> methods",
811                    )
812                    .to_compile_error()
813                    .into();
814                }
815                let expression = match attr.parse_args::<syn::Expr>() {
816                    Ok(expression) => expression,
817                    Err(error) => return error.to_compile_error().into(),
818                };
819                if method_timeout.replace(expression).is_some() {
820                    return syn::Error::new_spanned(attr, "duplicate request_timeout")
821                        .to_compile_error()
822                        .into();
823                }
824            }
825            method
826                .attrs
827                .retain(|a| !a.path().is_ident("request_timeout"));
828            let timeout = method_timeout
829                .or_else(|| protocol_timeout.clone())
830                .unwrap_or_else(
831                    || syn::parse_quote! { spawned_concurrency::response::DEFAULT_REQUEST_TIMEOUT },
832                );
833
834            let method_attrs: Vec<Attribute> = method
835                .attrs
836                .iter()
837                .filter(|a| {
838                    a.path().is_ident("doc")
839                        || a.path().is_ident("cfg")
840                        || a.path().is_ident("cfg_attr")
841                })
842                .cloned()
843                .collect();
844            let cfg_attrs: Vec<Attribute> = method
845                .attrs
846                .iter()
847                .filter(|a| a.path().is_ident("cfg") || a.path().is_ident("cfg_attr"))
848                .cloned()
849                .collect();
850
851            methods.push(ProtocolMethodInfo {
852                method_name,
853                struct_name,
854                field_names,
855                field_types,
856                kind,
857                params,
858                ret_type: method.sig.output.clone(),
859                method_attrs,
860                cfg_attrs,
861                timeout,
862            });
863        }
864    }
865
866    // Generate message structs
867    // Field types and result types are qualified with `super::` to prevent
868    // name collisions when a generated struct name shadows an imported type.
869    // e.g., `fn event(&self, event: Event)` generates `struct Event { pub event: super::Event }`
870    let msg_structs: Vec<_> = methods
871        .iter()
872        .map(|m| {
873            let struct_name = &m.struct_name;
874            let field_names = &m.field_names;
875            let qualified_field_types: Vec<Type> =
876                m.field_types.iter().map(qualify_type_with_super).collect();
877            let method_attrs = &m.method_attrs;
878            let cfg_attrs = &m.cfg_attrs;
879            let msg_result_ty: Type = match &m.kind {
880                MethodKind::Send => syn::parse_quote! { () },
881                MethodKind::Request(inner) => qualify_type_with_super(inner),
882            };
883
884            if field_names.is_empty() {
885                quote! {
886                    #(#method_attrs)*
887                    #[derive(Clone)]
888                    pub struct #struct_name;
889                    #(#cfg_attrs)*
890                    impl Message for #struct_name {
891                        type Result = #msg_result_ty;
892                    }
893                }
894            } else {
895                quote! {
896                    #(#method_attrs)*
897                    pub struct #struct_name {
898                        #(pub #field_names: #qualified_field_types,)*
899                    }
900                    #(#cfg_attrs)*
901                    impl Message for #struct_name {
902                        type Result = #msg_result_ty;
903                    }
904                }
905            }
906        })
907        .collect();
908
909    // Gate async impls using the dependency feature namespace, not the consumer.
910    let target_trait = format_ident!("__{}Target", trait_name);
911    let tasks = quote! { spawned_concurrency::tasks };
912    let threads = quote! { spawned_concurrency::threads };
913    let proto_info = ProtocolInfo {
914        trait_name,
915        mod_name: &mod_name,
916        ref_name: &ref_name,
917        converter_trait: &converter_trait,
918        converter_method: &converter_method,
919        target_trait: &target_trait,
920    };
921    let tasks_impl = generate_target_impl(&proto_info, &methods, &tasks, RuntimeMode::Tasks);
922    let threads_impl = generate_target_impl(&proto_info, &methods, &threads, RuntimeMode::Threads);
923    let adapters = generate_protocol_adapters(&proto_info, &methods, &trait_def);
924    let blanket_impls = quote! {
925        #adapters
926        spawned_concurrency::__spawned_async_items! { #tasks_impl }
927        #threads_impl
928    };
929
930    let ref_doc = format!(
931        "Type-erased reference to any actor implementing [`{trait_name}`].\n\n\
932         Use this type to store protocol references without depending on the concrete actor type."
933    );
934
935    // Protocol traits are consumed via blanket impls and dyn references,
936    // so rustc may report methods as unused during development.
937    let output = quote! {
938        #[allow(dead_code)]
939        #trait_def
940
941        #[doc = #ref_doc]
942        #trait_vis type #ref_name = ::std::sync::Arc<dyn #trait_name>;
943
944        #trait_vis mod #mod_name {
945            use super::*;
946            use spawned_concurrency::message::Message;
947            #(#msg_structs)*
948        }
949
950        #trait_vis trait #converter_trait {
951            fn #converter_method(&self) -> #ref_name;
952        }
953
954        impl #converter_trait for #ref_name {
955            fn #converter_method(&self) -> #ref_name {
956                ::std::sync::Arc::clone(self)
957            }
958        }
959
960        #blanket_impls
961    };
962
963    output.into()
964}
965
966/// Generates `impl Actor` and `Handler<M>` implementations from an annotated impl block.
967///
968/// Place `#[actor]` on an `impl MyStruct` block. The macro extracts annotated methods
969/// and generates the boilerplate needed to run the struct as an actor.
970///
971/// # Protocol Assertion
972///
973/// Use `protocol = TraitName` to verify at compile time that the actor fully implements
974/// a protocol (i.e., that `ActorRef<Self>` implements the protocol trait):
975///
976/// ```ignore
977/// #[actor(protocol = NameServerProtocol)]
978/// impl NameServer { /* ... */ }
979/// ```
980///
981/// The runtime (tasks vs threads) is inferred from whether any handler in the block
982/// is `async fn`. If all handlers are defined outside the `#[actor]` block, put at
983/// least one handler inside so the macro can detect the correct runtime.
984///
985/// For multiple protocols:
986/// ```ignore
987/// #[actor(protocol(RoomProtocol, UserProtocol))]
988/// impl ChatUser { /* ... */ }
989/// ```
990///
991/// # Handler Attributes
992///
993/// | Attribute | Use for |
994/// |-----------|---------|
995/// | `#[request_handler]` | Messages that expect a reply (`Response<T>`) |
996/// | `#[send_handler]` | Fire-and-forget messages |
997/// | `#[handler]` | Generic handler (works for either kind) |
998///
999/// Handler signature: `fn name(&mut self, msg: MessageType, ctx: &Context<Self>) -> ReturnType`
1000///
1001/// The return type must match the message's `Message::Result`. For request handlers
1002/// returning `()`, omit the return type.
1003///
1004/// # Lifecycle Hooks
1005///
1006/// | Attribute | Called |
1007/// |-----------|--------|
1008/// | `#[started]` | After the actor starts, before processing messages |
1009/// | `#[stopped]` | After the actor stops processing messages |
1010/// | `#[exit_received]` | A linked exit delivered while trapping exits |
1011///
1012/// Both receive `&mut self` and `&Context<Self>`. Use async or sync to match your runtime.
1013///
1014/// # Example
1015///
1016/// ```ignore
1017/// use spawned_concurrency::tasks::{Actor, Context, Handler};
1018/// use spawned_concurrency::actor;
1019///
1020/// pub struct MyActor { count: u64 }
1021///
1022/// #[actor(protocol = CounterProtocol)]
1023/// impl MyActor {
1024///     pub fn new() -> Self { MyActor { count: 0 } }
1025///
1026///     #[started]
1027///     async fn on_start(&mut self, _ctx: &Context<Self>) {
1028///         tracing::info!("Actor started");
1029///     }
1030///
1031///     #[send_handler]
1032///     async fn handle_increment(&mut self, msg: Increment, _ctx: &Context<Self>) {
1033///         self.count += msg.amount;
1034///     }
1035///
1036///     #[request_handler]
1037///     async fn handle_get_count(&mut self, _msg: GetCount, _ctx: &Context<Self>) -> u64 {
1038///         self.count
1039///     }
1040/// }
1041/// ```
1042#[proc_macro_attribute]
1043pub fn actor(attr: TokenStream, item: TokenStream) -> TokenStream {
1044    let mut impl_block = parse_macro_input!(item as ItemImpl);
1045
1046    let self_ty = &impl_block.self_ty;
1047    let (impl_generics, _, where_clause) = impl_block.generics.split_for_impl();
1048
1049    // --- Parse named parameters from #[actor(protocol = X)] or #[actor(protocol(X, Y))] ---
1050    let bridge_traits: Vec<Ident> = if attr.is_empty() {
1051        Vec::new()
1052    } else {
1053        let parser = |input: syn::parse::ParseStream| -> syn::Result<Vec<Ident>> {
1054            let mut protocols = Vec::new();
1055            while !input.is_empty() {
1056                let key: Ident = input.parse()?;
1057                if key != "protocol" {
1058                    return Err(syn::Error::new(
1059                        key.span(),
1060                        "unknown parameter, expected `protocol`",
1061                    ));
1062                }
1063                if input.peek(syn::Token![=]) {
1064                    // protocol = TraitName
1065                    let _: syn::Token![=] = input.parse()?;
1066                    protocols.push(input.parse()?);
1067                } else {
1068                    // protocol(Trait1, Trait2)
1069                    let content;
1070                    syn::parenthesized!(content in input);
1071                    let punctuated = content.parse_terminated(Ident::parse, syn::Token![,])?;
1072                    protocols.extend(punctuated);
1073                }
1074                if input.peek(syn::Token![,]) {
1075                    let _: syn::Token![,] = input.parse()?;
1076                }
1077            }
1078            Ok(protocols)
1079        };
1080        match syn::parse::Parser::parse(parser, attr) {
1081            Ok(traits) => traits,
1082            Err(e) => return e.to_compile_error().into(),
1083        }
1084    };
1085
1086    // --- Extract #[started] and #[stopped] lifecycle methods ---
1087    let mut started_method: Option<ImplItemFn> = None;
1088    let mut stopped_method: Option<ImplItemFn> = None;
1089    let mut exit_method: Option<ImplItemFn> = None;
1090    let mut has_async = false;
1091
1092    let mut items_to_keep = Vec::new();
1093    for item in impl_block.items.drain(..) {
1094        if let ImplItem::Fn(ref method) = item {
1095            let is_started = method.attrs.iter().any(|a| a.path().is_ident("started"));
1096            let is_stopped = method.attrs.iter().any(|a| a.path().is_ident("stopped"));
1097
1098            let is_exit = method
1099                .attrs
1100                .iter()
1101                .any(|a| a.path().is_ident("exit_received"));
1102            if is_exit {
1103                if exit_method.is_some() {
1104                    return syn::Error::new_spanned(
1105                        &method.sig,
1106                        "only one #[exit_received] method is allowed per actor",
1107                    )
1108                    .to_compile_error()
1109                    .into();
1110                }
1111                if is_started
1112                    || is_stopped
1113                    || method.attrs.iter().any(|a| {
1114                        a.path().is_ident("handler")
1115                            || a.path().is_ident("send_handler")
1116                            || a.path().is_ident("request_handler")
1117                    })
1118                {
1119                    return syn::Error::new_spanned(&method.sig, "#[exit_received] cannot be combined with other lifecycle or handler attributes").to_compile_error().into();
1120                }
1121                if method.sig.inputs.len() != 3
1122                    || !matches!(method.sig.inputs.first(), Some(FnArg::Receiver(r)) if r.reference.is_some() && r.mutability.is_some())
1123                {
1124                    return syn::Error::new_spanned(
1125                        &method.sig,
1126                        "#[exit_received] must take (&mut self, Exit, &Context<Self>)",
1127                    )
1128                    .to_compile_error()
1129                    .into();
1130                }
1131                let mut m = method.clone();
1132                m.attrs.retain(|a| !a.path().is_ident("exit_received"));
1133                m.vis = syn::Visibility::Inherited;
1134                m.sig.ident = format_ident!("exit_received");
1135                if m.sig.asyncness.is_some() {
1136                    has_async = true;
1137                }
1138                exit_method = Some(m);
1139                continue;
1140            }
1141
1142            if is_started {
1143                if started_method.is_some() {
1144                    return syn::Error::new_spanned(
1145                        &method.sig,
1146                        "only one #[started] method is allowed per actor",
1147                    )
1148                    .to_compile_error()
1149                    .into();
1150                }
1151                if method.attrs.iter().any(|a| {
1152                    a.path().is_ident("handler")
1153                        || a.path().is_ident("send_handler")
1154                        || a.path().is_ident("request_handler")
1155                }) {
1156                    return syn::Error::new_spanned(
1157                        &method.sig,
1158                        "#[started] cannot be combined with handler attributes",
1159                    )
1160                    .to_compile_error()
1161                    .into();
1162                }
1163                // Expect: fn started(&mut self, ctx: &Context<Self>)
1164                if method.sig.inputs.len() != 2 {
1165                    return syn::Error::new_spanned(
1166                        &method.sig,
1167                        "#[started] method must take exactly (&mut self, &Context<Self>)",
1168                    )
1169                    .to_compile_error()
1170                    .into();
1171                }
1172                if !matches!(method.sig.inputs.first(), Some(FnArg::Receiver(r)) if r.mutability.is_some())
1173                {
1174                    return syn::Error::new_spanned(
1175                        &method.sig,
1176                        "#[started] method's first parameter must be `&mut self`",
1177                    )
1178                    .to_compile_error()
1179                    .into();
1180                }
1181                let mut m = method.clone();
1182                m.attrs.retain(|a| !a.path().is_ident("started"));
1183                m.vis = syn::Visibility::Inherited;
1184                m.sig.ident = format_ident!("started");
1185                if m.sig.asyncness.is_some() {
1186                    has_async = true;
1187                }
1188                started_method = Some(m);
1189                continue;
1190            }
1191
1192            if is_stopped {
1193                if stopped_method.is_some() {
1194                    return syn::Error::new_spanned(
1195                        &method.sig,
1196                        "only one #[stopped] method is allowed per actor",
1197                    )
1198                    .to_compile_error()
1199                    .into();
1200                }
1201                if method.attrs.iter().any(|a| {
1202                    a.path().is_ident("handler")
1203                        || a.path().is_ident("send_handler")
1204                        || a.path().is_ident("request_handler")
1205                }) {
1206                    return syn::Error::new_spanned(
1207                        &method.sig,
1208                        "#[stopped] cannot be combined with handler attributes",
1209                    )
1210                    .to_compile_error()
1211                    .into();
1212                }
1213                // Expect: fn stopped(&mut self, ctx: &Context<Self>)
1214                if method.sig.inputs.len() != 2 {
1215                    return syn::Error::new_spanned(
1216                        &method.sig,
1217                        "#[stopped] method must take exactly (&mut self, &Context<Self>)",
1218                    )
1219                    .to_compile_error()
1220                    .into();
1221                }
1222                if !matches!(method.sig.inputs.first(), Some(FnArg::Receiver(r)) if r.mutability.is_some())
1223                {
1224                    return syn::Error::new_spanned(
1225                        &method.sig,
1226                        "#[stopped] method's first parameter must be `&mut self`",
1227                    )
1228                    .to_compile_error()
1229                    .into();
1230                }
1231                let mut m = method.clone();
1232                m.attrs.retain(|a| !a.path().is_ident("stopped"));
1233                m.vis = syn::Visibility::Inherited;
1234                m.sig.ident = format_ident!("stopped");
1235                if m.sig.asyncness.is_some() {
1236                    has_async = true;
1237                }
1238                stopped_method = Some(m);
1239                continue;
1240            }
1241        }
1242        items_to_keep.push(item);
1243    }
1244    impl_block.items = items_to_keep;
1245
1246    // --- Process handler methods ---
1247    let mut handler_impls = Vec::new();
1248
1249    for item in &mut impl_block.items {
1250        if let ImplItem::Fn(method) = item {
1251            let handler_idx = method.attrs.iter().position(|attr| {
1252                attr.path().is_ident("handler")
1253                    || attr.path().is_ident("send_handler")
1254                    || attr.path().is_ident("request_handler")
1255            });
1256
1257            if let Some(idx) = handler_idx {
1258                method.attrs.remove(idx);
1259
1260                // Collect remaining attributes (e.g. #[cfg(...)]) to propagate
1261                // to the generated Handler impl block.
1262                let extra_attrs: Vec<_> = method
1263                    .attrs
1264                    .iter()
1265                    .filter(|a| {
1266                        !a.path().is_ident("handler")
1267                            && !a.path().is_ident("send_handler")
1268                            && !a.path().is_ident("request_handler")
1269                            && !a.path().is_ident("started")
1270                            && !a.path().is_ident("stopped")
1271                    })
1272                    .cloned()
1273                    .collect();
1274
1275                let method_name = &method.sig.ident;
1276                if method.sig.asyncness.is_some() {
1277                    has_async = true;
1278                }
1279
1280                // Validate handler has exactly 3 parameters: &mut self, msg, ctx
1281                let param_count = method.sig.inputs.len();
1282                if param_count != 3 {
1283                    return syn::Error::new_spanned(
1284                        &method.sig,
1285                        format!(
1286                            "handler method must have 3 parameters (&mut self, msg: M, ctx: &Context<Self>), found {param_count}"
1287                        ),
1288                    )
1289                    .to_compile_error()
1290                    .into();
1291                }
1292
1293                // Extract message type from 2nd parameter (index 1, after &mut self)
1294                let msg_ty = match method.sig.inputs.iter().nth(1) {
1295                    Some(FnArg::Typed(pat_type)) => &*pat_type.ty,
1296                    _ => {
1297                        return syn::Error::new_spanned(
1298                            &method.sig,
1299                            "handler method must have signature: fn(&mut self, msg: M, ctx: &Context<Self>) -> R",
1300                        )
1301                        .to_compile_error()
1302                        .into();
1303                    }
1304                };
1305
1306                // Extract return type (default to () if omitted)
1307                let ret_ty: Box<Type> = match &method.sig.output {
1308                    ReturnType::Default => syn::parse_quote! { () },
1309                    ReturnType::Type(_, ty) => ty.clone(),
1310                };
1311
1312                let handler_impl = if method.sig.asyncness.is_some() {
1313                    quote! {
1314                        #(#extra_attrs)*
1315                        impl #impl_generics Handler<#msg_ty> for #self_ty #where_clause {
1316                            async fn handle(&mut self, msg: #msg_ty, ctx: &Context<Self>) -> #ret_ty {
1317                                self.#method_name(msg, ctx).await
1318                            }
1319                        }
1320                    }
1321                } else {
1322                    quote! {
1323                        #(#extra_attrs)*
1324                        impl #impl_generics Handler<#msg_ty> for #self_ty #where_clause {
1325                            fn handle(&mut self, msg: #msg_ty, ctx: &Context<Self>) -> #ret_ty {
1326                                self.#method_name(msg, ctx)
1327                            }
1328                        }
1329                    }
1330                };
1331
1332                handler_impls.push(handler_impl);
1333            }
1334        }
1335    }
1336
1337    // --- Generate impl Actor ---
1338    let lifecycle_methods: Vec<&ImplItemFn> = [
1339        started_method.as_ref(),
1340        stopped_method.as_ref(),
1341        exit_method.as_ref(),
1342    ]
1343    .into_iter()
1344    .flatten()
1345    .collect();
1346
1347    let protocol_doc = if bridge_traits.is_empty() {
1348        quote! {}
1349    } else {
1350        let lines: Vec<String> = bridge_traits.iter().map(|t| format!("- [`{t}`]")).collect();
1351        let doc_body = format!(
1352            "# Protocol\n\n\
1353             When started, `ActorRef<{ty}>` implements:\n\n\
1354             {lines}\n\n\
1355             See the protocol trait docs for the full API.",
1356            ty = quote!(#self_ty),
1357            lines = lines.join("\n"),
1358        );
1359        quote! { #[doc = #doc_body] }
1360    };
1361
1362    let actor_impl = quote! {
1363        #protocol_doc
1364        impl #impl_generics Actor for #self_ty #where_clause {
1365            #(#lifecycle_methods)*
1366        }
1367    };
1368
1369    // --- Generate bridge assertions ---
1370    let runtime_path = if has_async {
1371        quote! { spawned_concurrency::tasks }
1372    } else {
1373        quote! { spawned_concurrency::threads }
1374    };
1375
1376    let bridge_asserts: Vec<_> = bridge_traits
1377        .iter()
1378        .map(|trait_name| {
1379            quote! {
1380                const _: () = {
1381                    fn _assert_bridge<__T: #trait_name>() {}
1382                    fn _check() {
1383                        _assert_bridge::<#runtime_path::ActorRef<#self_ty>>();
1384                    }
1385                };
1386            }
1387        })
1388        .collect();
1389
1390    let output = quote! {
1391        #actor_impl
1392        #impl_block
1393        #(#handler_impls)*
1394        #(#bridge_asserts)*
1395    };
1396
1397    output.into()
1398}