rogue_macros/
lib.rs

1//! # runtime‑macros
2//!
3//! Procedural macros used by the **runtime** project to generate RPC stubs, strongly‑typed
4//! message‑handlers, and boilerplate for object references at **compile‑time**.  
5//! They contain **no runtime logic** – their sole purpose is to expand user‑written async
6//! functions and `impl` blocks into the code required for inter‑runtime communication.
7//!
8//! ## What it generates
9//! * A [`MessageHandler`] implementation that the runtime can dispatch.
10//! * `Input` and `Output` structs that implement [`TypeInfo`] for binary serialization.
11//! * A tiny, ergonomic forwarding stub that performs the actual RPC call.
12//!
13//! The generated pieces are completely hidden from the end‑user, giving them a clean API while
14//! retaining full type‑safety over the wire.
15//!
16//! ## Usage
17//! Put async functions or methods inside an `rpc! { ... }` block:
18//! ```rust,ignore
19//! rpc! {
20//!     async fn add(a: i32, b: i32) -> i32 {
21//!         a + b
22//!     }
23//! }
24//! ```.
25use heck::ToPascalCase;
26use proc_macro::TokenStream;
27use proc_macro_error::proc_macro_error;
28use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
29use quote::{ToTokens, quote};
30use syn::{
31    Attribute, Item, ItemStruct, Result,
32    fold::Fold,
33    parse::{Parse, ParseStream},
34    parse_macro_input,
35};
36
37/// Returns `true` if the attribute list contains `#[local]`.
38fn has_local_attr(attrs: &[Attribute]) -> bool {
39    attrs.iter().any(|a| a.path().is_ident("local"))
40}
41
42/// Removes all `#[local]` attributes from the list in‑place.
43fn remove_local_attr(attrs: &mut Vec<Attribute>) {
44    attrs.retain(|a| !a.path().is_ident("local"));
45}
46
47/// Generate the body of an RPC‑forwarding stub.
48///
49/// The returned `TokenStream2` is the method body that:
50/// 1. Builds the `args` input struct.
51/// 2. Calls `ctx.runtime.call::<Handler>(target_id, args).await`.
52/// 3. Emits tracing spans and converts errors into a `pending` future.
53fn build_rpc_call_body(
54    input_ident: &Ident,
55    handler_ident: &Ident,
56    fld_inits: &[TokenStream2],
57    target_expr: TokenStream2,
58    rpc_name: TokenStream2,
59    ret_ty: TokenStream2,
60) -> TokenStream2 {
61    quote! {
62        let ctx = rogue_runtime::CONTEXT.get();
63        let args = #input_ident { #(#fld_inits),* };
64        let target_id = #target_expr;
65        rogue_runtime::tracing::trace!(rpc = #rpc_name, target = %target_id, "calling");
66        let ret = match ctx.runtime.call::<#handler_ident>(target_id.clone(), args).await {
67            Ok(output) => output.0,
68            Err(e) => {
69                rogue_runtime::tracing::error!(rpc = #rpc_name, target = %target_id, error = %e.to_string(), "failed");
70                let _ = ctx.error_tx.send(e).await;
71                return ::std::future::pending::<#ret_ty>().await;
72            }
73        };
74        rogue_runtime::tracing::trace!(rpc = #rpc_name, target = %target_id, "done");
75        ret
76    }
77}
78
79/// Helper that produces the boiler‑plate `TypeInfo` implementation for a
80/// given identifier, eliminating the repetition of the `type_name` and
81/// `type_id` methods throughout the macro code.
82fn gen_type_info_impl(ident: &Ident) -> TokenStream2 {
83    quote! {
84        impl rogue_runtime::TypeInfo for #ident {
85            fn type_name() -> &'static str { stringify!(#ident) }
86            fn type_id() -> rogue_runtime::TypeId {
87                <rogue_runtime::sha2::Sha256 as rogue_runtime::sha2::Digest>::digest(Self::type_name()).into()
88            }
89        }
90    }
91}
92
93/// Recursively search a type for any occurrence of `Self`.
94fn type_contains_self(ty: &syn::Type) -> bool {
95    use syn::visit::Visit;
96    struct Finder {
97        found: bool,
98    }
99    impl<'ast> Visit<'ast> for Finder {
100        fn visit_type_path(&mut self, tp: &'ast syn::TypePath) {
101            if tp.qself.is_none()
102                && tp.path.segments.len() == 1
103                && tp.path.segments[0].ident == "Self"
104            {
105                self.found = true;
106            }
107            syn::visit::visit_type_path(self, tp);
108        }
109    }
110    let mut finder = Finder { found: false };
111    Finder::visit_type(&mut finder, ty);
112    finder.found
113}
114
115/* -------- mini-AST: just “a list of items inside the block” -------- */
116
117/// Minimal wrapper around a sequence of Rust items that appear inside an
118/// `rpc! { ... }` block. The parser simply collects everything until the end
119/// of the block so the macro can iterate over the items later on.
120struct RpcBlock {
121    items: Vec<Item>,
122}
123
124impl Parse for RpcBlock {
125    fn parse(input: ParseStream) -> Result<Self> {
126        let mut items = Vec::new();
127        while !input.is_empty() {
128            items.push(input.parse()?);
129        }
130        Ok(Self { items })
131    }
132}
133
134/* -------- the procedural macro -------- */
135
136/// Generates the glue code that turns a user‑written async function or method
137/// into a fully‑fledged RPC endpoint understood by the runtime.
138///
139/// The returned [`TokenStream2`] contains:
140/// * A [`MessageHandler`] implementation that owns the actual async `handle`
141///   logic and is registered through `inventory`.
142/// * Typed `Input` and `Output` structs that carry the arguments and return
143///   value across the wire and implement [`TypeInfo`].
144/// * (When required) an ergonomic forwarding stub that clients call instead
145///   of the original function, which performs the RPC under the hood.
146///
147/// # Parameters
148/// * `fn_ident` – Identifier of the original async function/method.
149/// * `impl_ident_opt` – `Some` with the containing struct’s ident for
150///   methods, or `None` for free functions.
151/// * `inputs` – Punctuated list of the function’s parameters.
152/// * `output` – The declared return type.
153fn generate_message_handler(
154    fn_ident: &Ident,
155    impl_ident_opt: Option<&Ident>,
156    inputs: &syn::punctuated::Punctuated<syn::FnArg, syn::token::Comma>,
157    output: &syn::ReturnType,
158) -> TokenStream2 {
159    // strip `_impl` suffix and convert to PascalCase
160    let pascal = fn_ident
161        .to_string()
162        .trim_end_matches("_impl")
163        .to_pascal_case();
164
165    // If this comes from an `impl` block, include the (trimmed) struct name as prefix
166    let struct_prefix = if let Some(impl_ident) = impl_ident_opt {
167        let mut s = impl_ident.to_string();
168        // our macro renames structs to `<Orig>Impl`; strip the suffix for readability
169        if s.ends_with("Impl") {
170            s.truncate(s.len() - 4);
171        }
172        s
173    } else {
174        String::new()
175    };
176
177    // original (alias) struct name – e.g. "Calculator" – used for `Self` returns
178    let alias_ident = if !struct_prefix.is_empty() {
179        Ident::new(&struct_prefix, fn_ident.span())
180    } else {
181        // dummy ident; won’t be used for free functions
182        Ident::new("_Unused", fn_ident.span())
183    };
184
185    let handler_ident = Ident::new(
186        &format!("{}{}Handler", struct_prefix, pascal),
187        fn_ident.span(),
188    );
189    let input_ident = Ident::new(
190        &format!("{}{}Input", struct_prefix, pascal),
191        fn_ident.span(),
192    );
193    let output_ident = Ident::new(
194        &format!("{}{}Output", struct_prefix, pascal),
195        fn_ident.span(),
196    );
197
198    // Pre‑compute `TypeInfo` implementations via the helper to avoid repeating
199    // the same quoted code three times.
200    let handler_ti = gen_type_info_impl(&handler_ident);
201    let input_ti = gen_type_info_impl(&input_ident);
202    let output_ti = gen_type_info_impl(&output_ident);
203
204    // ----- build input‑struct fields & argument list -------------------------
205    let mut arg_fields: Vec<TokenStream2> = Vec::new();
206    let mut arg_names: Vec<Ident> = Vec::new();
207    let mut has_receiver = false;
208    let mut consumes_self = false;
209    let mut is_mut_receiver = false;
210
211    for arg in inputs {
212        match arg {
213            syn::FnArg::Receiver(recv) => {
214                has_receiver = true;
215                if recv.reference.is_none() {
216                    consumes_self = true; // by-value `self`
217                } else if recv.mutability.is_some() {
218                    is_mut_receiver = true; // `&mut self`
219                }
220            }
221            syn::FnArg::Typed(pat_ty) => {
222                if let syn::Pat::Ident(pat_ident) = &*pat_ty.pat {
223                    let nm = &pat_ident.ident;
224                    let ty = &pat_ty.ty;
225                    arg_fields.push(quote! { #nm: #ty });
226                    arg_names.push(nm.clone());
227                }
228            }
229        }
230    }
231
232    // Choose RwLock access mode for &self / &mut self
233    let lock_method = if is_mut_receiver {
234        quote! { write }
235    } else {
236        quote! { read }
237    };
238
239    // prepend instance id for &self methods
240    if has_receiver {
241        arg_fields.insert(0, quote! { __instance_id: rogue_runtime::InstanceId });
242    }
243
244    // When there are no arguments the repetition expands to nothing, so we no
245    // longer need a special‑case branch.
246    let input_struct_fields = quote! { #(pub #arg_fields,)* };
247
248    // Helper that replaces every `Self` in a type with the generated alias
249    struct ReplaceSelf<'a> {
250        alias: &'a Ident,
251    }
252
253    impl<'a> Fold for ReplaceSelf<'a> {
254        fn fold_type_path(&mut self, mut tp: syn::TypePath) -> syn::TypePath {
255            if tp.qself.is_none()
256                && tp.path.segments.len() == 1
257                && tp.path.segments[0].ident == "Self"
258            {
259                tp.path.segments[0].ident = self.alias.clone();
260            }
261            // continue walking so nested generics are handled, too
262            syn::fold::fold_type_path(self, tp)
263        }
264    }
265
266    let (ret_ty, needs_auto_register) = match output {
267        syn::ReturnType::Type(_, ty) => {
268            // Replace every occurrence of `Self` inside the type with the alias
269            let mut replacer = ReplaceSelf {
270                alias: &alias_ident,
271            };
272            let new_ty = replacer.fold_type((**ty).clone());
273
274            let needs_auto_register = type_contains_self(&**ty);
275
276            (quote! { #new_ty }, needs_auto_register)
277        }
278        syn::ReturnType::Default => (quote! { () }, false),
279    };
280
281    // Build the expressions we will actually pass to the wrapped function —
282    // they must reference the fields on `data`, e.g. `data.n`
283    let arg_values: Vec<TokenStream2> = arg_names.iter().map(|nm| quote! { data.#nm }).collect();
284    // body of MessageHandler::handle -----------------------------------------
285    let call_site = if has_receiver {
286        let impl_ident = impl_ident_opt.expect("impl ident required for method with receiver");
287        if consumes_self {
288            if needs_auto_register {
289                quote! {
290                    let ctx = rogue_runtime::CONTEXT.get();
291                    match ctx.runtime.take_instance::<#impl_ident>(data.__instance_id).await {
292                        Some(instance) => {
293                            let res = instance.#fn_ident(#(#arg_values),*).await;
294                            let res = res.register_instance(&ctx.runtime).await;
295                            Ok(#output_ident(res))
296                        }
297                        None => Err("instance not found".into()),
298                    }
299                }
300            } else {
301                quote! {
302                    let ctx = rogue_runtime::CONTEXT.get();
303                    match ctx.runtime.take_instance::<#impl_ident>(data.__instance_id).await {
304                        Some(instance) => {
305                            let res = instance.#fn_ident(#(#arg_values),*).await;
306                            Ok(#output_ident(res))
307                        }
308                        None => Err("instance not found".into()),
309                    }
310                }
311            }
312        } else if needs_auto_register {
313            quote! {
314                let ctx = rogue_runtime::CONTEXT.get();
315                match ctx.runtime.get_instance::<#impl_ident>(data.__instance_id).await {
316                    Ok(instance) => {
317                        let res = instance.#lock_method().await.#fn_ident(#(#arg_values),*).await;
318                        let res = res.register_instance(&ctx.runtime).await;
319                        Ok(#output_ident(res))
320                    }
321                    Err(e) => Err(e),
322                }
323            }
324        } else {
325            quote! {
326                let ctx = rogue_runtime::CONTEXT.get();
327                match ctx.runtime.get_instance::<#impl_ident>(data.__instance_id).await {
328                    Ok(instance) => {
329                        let res = instance.#lock_method().await.#fn_ident(#(#arg_values),*).await;
330                        Ok(#output_ident(res))
331                    }
332                    Err(e) => Err(e),
333                }
334            }
335        }
336    } else if let Some(impl_ident) = impl_ident_opt {
337        if needs_auto_register {
338            quote! {
339                let ctx = rogue_runtime::CONTEXT.get();
340                let res = #impl_ident::#fn_ident(#(#arg_values),*).await;
341                let res = res.register_instance(&ctx.runtime).await;
342                Ok(#output_ident(res))
343            }
344        } else {
345            quote! {
346                let res = #impl_ident::#fn_ident(#(#arg_values),*).await;
347                Ok(#output_ident(res))
348            }
349        }
350    } else {
351        // free async function
352        quote! {
353            let res = #fn_ident(#(#arg_values),*).await;
354            Ok(#output_ident(res))
355        }
356    };
357
358    // final token stream -----------------------------------------------------
359    quote! {
360        struct #handler_ident;
361
362        #handler_ti
363
364        #[derive(rogue_runtime::serde::Deserialize, rogue_runtime::serde::Serialize)]
365        struct #input_ident { #input_struct_fields }
366
367        #input_ti
368
369        #[allow(non_camel_case_types)]
370        #[derive(rogue_runtime::serde::Deserialize, rogue_runtime::serde::Serialize)]
371        struct #output_ident(#ret_ty);
372
373        #output_ti
374
375        impl rogue_runtime::InventoryItemProvider for #handler_ident {
376            fn type_info() -> rogue_runtime::InventoryItem {
377                rogue_runtime::InventoryItem::Function(rogue_runtime::InventoryFunctionItem::new(stringify!(#handler_ident).to_string()))
378            }
379        }
380
381        #[rogue_runtime::async_trait]
382        impl rogue_runtime::MessageHandler for #handler_ident {
383            type Input  = #input_ident;
384            type Output = #output_ident;
385
386            async fn handle(data: Self::Input) -> std::result::Result<Self::Output, String> {
387                #call_site
388            }
389        }
390
391        rogue_runtime::inventory::submit! {
392            rogue_runtime::HandlerRegistration {
393                register: |rt: rogue_runtime::Runtime| {
394                    Box::pin(async move {
395                        rt.register_handler::<#handler_ident>().await
396                    })
397                },
398            }
399        }
400    }
401}
402
403/// Procedural macro `rpc!` – transforms async functions and methods into
404/// remote‑callable RPC endpoints.
405///
406/// The macro walks every item inside the supplied block:
407/// * Async functions/methods become RPC handlers plus forwarding stubs.
408/// * Structs are renamed and wrapped in `ObjectRef` type aliases.
409/// * Impl blocks are duplicated into trait definitions so both concrete
410///   instances and their `ObjectRef` stubs implement the same API.
411///
412/// Items annotated with `#[local]` are passed through unchanged, allowing
413/// authors to mix local utilities and remote calls within the same block.
414///
415/// See the crate‑level docs for a full usage example.
416#[proc_macro_error]
417#[proc_macro]
418pub fn rpc(input: TokenStream) -> TokenStream {
419    let RpcBlock { items } = parse_macro_input!(input as RpcBlock);
420
421    /* ---------- first pass: gather struct renames ---------- */
422    use std::collections::HashMap;
423
424    // map OriginalName -> GeneratedNameImpl
425    let mut struct_map: HashMap<String, Ident> = HashMap::new();
426    for item in &items {
427        if let Item::Struct(ItemStruct { ident, .. }) = item {
428            let impl_ident = Ident::new(&format!("{}Impl", ident), ident.span());
429            struct_map.insert(ident.to_string(), impl_ident);
430        }
431    }
432
433    /* containers for emitted tokens */
434    let mut emitted: Vec<TokenStream2> = Vec::new();
435    let mut extra: Vec<TokenStream2> = Vec::new(); // generated extras (type alias, traits …)
436
437    /* ---------- second pass: transform items ---------- */
438    for mut item in items {
439        match &mut item {
440            /* ---- free functions --------------------------------------------------------- */
441            Item::Fn(f) => {
442                let local = has_local_attr(&f.attrs);
443                if local {
444                    let mut func = f.clone();
445                    remove_local_attr(&mut func.attrs);
446                    emitted.push(func.into_token_stream());
447                    continue;
448                }
449                if f.sig.asyncness.is_some() {
450                    // rename original to foo_impl
451                    let orig_ident = f.sig.ident.clone();
452                    let orig_vis = f.vis.clone();
453                    let impl_ident = Ident::new(&format!("{}_impl", orig_ident), orig_ident.span());
454
455                    // change the original function’s ident & visibility
456                    f.sig.ident = impl_ident.clone();
457                    f.vis = syn::Visibility::Inherited;
458
459                    // ---- generate the MessageHandler BEFORE we move `item` ----------
460                    let handler_ts =
461                        generate_message_handler(&f.sig.ident, None, &f.sig.inputs, &f.sig.output);
462
463                    // --- build a forwarding stub that performs an RPC call ------------------
464                    let mut stub_sig = f.sig.clone();
465                    stub_sig.ident = orig_ident.clone();
466
467                    /* -------- collect argument idents & build Input struct init ------------ */
468                    let mut fld_inits: Vec<TokenStream2> = Vec::new();
469                    for arg in &stub_sig.inputs {
470                        if let syn::FnArg::Typed(pt) = arg {
471                            if let syn::Pat::Ident(pat_ident) = &*pt.pat {
472                                let nm = &pat_ident.ident;
473                                fld_inits.push(quote! { #nm: #nm });
474                            }
475                        }
476                    }
477
478                    /* -------- Handler/Input idents ---------------------------------------- */
479                    let pascal = orig_ident.to_string().to_pascal_case();
480                    let handler_ident =
481                        Ident::new(&format!("{}Handler", pascal), Span::call_site());
482                    let input_ident = Ident::new(&format!("{}Input", pascal), Span::call_site());
483
484                    /* -------- return type tokens ----------------------------------------- */
485                    let ret_ty_tokens: TokenStream2 = match &stub_sig.output {
486                        syn::ReturnType::Type(_, ty) => quote! { #ty },
487                        syn::ReturnType::Default => quote! { () },
488                    };
489
490                    /* -------- build stub body -------------------------------------------- */
491                    let rpc_name = quote! { stringify!(#orig_ident) };
492                    let body_stmts = build_rpc_call_body(
493                        &input_ident,
494                        &handler_ident,
495                        &fld_inits,
496                        quote! { ctx.target_id.clone() },
497                        rpc_name,
498                        quote! { #ret_ty_tokens },
499                    );
500
501                    let stub_fn = quote! {
502                        #orig_vis #stub_sig {
503                            #body_stmts
504                        }
505                    };
506
507                    emitted.push(item.into_token_stream());
508                    emitted.push(stub_fn);
509                    extra.push(handler_ts);
510                } else {
511                    emitted.push(item.into_token_stream());
512                }
513            }
514
515            /* ---- structs --------------------------------------------------------------- */
516            Item::Struct(strct) => {
517                let orig_ident = strct.ident.clone();
518                let orig_vis = strct.vis.clone();
519                let impl_ident = struct_map.get(&orig_ident.to_string()).unwrap().clone();
520                strct.ident = impl_ident.clone(); // rename struct inside original item
521                emitted.push(item.into_token_stream());
522
523                // type alias
524                let alias = quote! {
525                    #orig_vis type #orig_ident = rogue_runtime::ObjectRef<#impl_ident>;
526                };
527                extra.push(alias);
528            }
529
530            /* ---- impl blocks ----------------------------------------------------------- */
531            Item::Impl(imp) => {
532                // does this impl target one of our renamed structs?
533                let self_ty_ident_opt = match &*imp.self_ty {
534                    syn::Type::Path(p) => p.path.get_ident().cloned(),
535                    _ => None,
536                };
537                // If the whole impl is marked #[local], just rename the target type (if needed)
538                // and pass it through without generating RPC wrappers.
539                let impl_is_local = has_local_attr(&imp.attrs);
540                if impl_is_local {
541                    remove_local_attr(&mut imp.attrs);
542                    if let Some(self_ident) = self_ty_ident_opt.clone() {
543                        if let Some(impl_ident) = struct_map.get(&self_ident.to_string()) {
544                            let new_ty: syn::Type = syn::parse_quote!(#impl_ident);
545                            imp.self_ty = Box::new(new_ty);
546                        }
547                    }
548                    emitted.push(item.into_token_stream());
549                    continue;
550                }
551                if let Some(self_ident) = self_ty_ident_opt {
552                    if let Some(impl_ident) = struct_map.get(&self_ident.to_string()) {
553                        /* rename target type in the impl */
554                        let new_ty: syn::Type = syn::parse_quote!(#impl_ident);
555                        imp.self_ty = Box::new(new_ty);
556
557                        /* buckets for methods */
558                        let mut rpc_methods: Vec<syn::ImplItem> = Vec::new();
559                        let mut keep_methods: Vec<syn::ImplItem> = Vec::new();
560
561                        // Is this `impl SomeTrait for Type` ?
562                        let is_trait_impl = imp.trait_.is_some();
563
564                        for mut itm in std::mem::take(&mut imp.items) {
565                            if let syn::ImplItem::Fn(ref mut m) = itm {
566                                let async_ = m.sig.asyncness.is_some();
567                                let local = has_local_attr(&m.attrs);
568                                if async_ && !local {
569                                    remove_local_attr(&mut m.attrs);
570
571                                    // clone for RPC handler generation
572                                    rpc_methods.push(itm.clone());
573
574                                    // keep original method bodies *only* for trait impls so
575                                    // server‑side logic is still present
576                                    if is_trait_impl {
577                                        keep_methods.push(itm);
578                                    }
579                                    // for inherent impls we strip the method here – it will
580                                    // re‑appear inside the generated `impl <Trait> for Type`
581                                } else {
582                                    remove_local_attr(&mut m.attrs);
583                                    keep_methods.push(itm);
584                                }
585                            } else {
586                                keep_methods.push(itm);
587                            }
588                        }
589                        imp.items = keep_methods;
590
591                        /* ensure async‑trait on impl <Trait> for <Type> */
592                        if is_trait_impl {
593                            let has_async_trait_attr = imp.attrs.iter().any(|a| {
594                                a.path()
595                                    .segments
596                                    .last()
597                                    .map(|s| s.ident == "async_trait")
598                                    .unwrap_or(false)
599                            });
600                            if !has_async_trait_attr {
601                                imp.attrs
602                                    .insert(0, syn::parse_quote!(#[rogue_runtime::async_trait]));
603                            }
604                        }
605
606                        /* generate trait or stub‑only impls when we collected async RPC methods */
607                        if !rpc_methods.is_empty() {
608                            let mut trait_items: Vec<TokenStream2> = Vec::new();
609                            let mut impl_items: Vec<TokenStream2> = Vec::new();
610                            let mut stub_items: Vec<TokenStream2> = Vec::new();
611
612                            for itm in &rpc_methods {
613                                if let syn::ImplItem::Fn(m) = itm {
614                                    /* ------------- signature for generated trait (inherent impls) --------- */
615                                    let mut sig_trait = m.sig.clone();
616
617                                    let mut needs_auto_register = false;
618                                    if let syn::ReturnType::Type(_, ty) = &sig_trait.output {
619                                        if type_contains_self(&**ty) {
620                                            needs_auto_register = true;
621                                        }
622                                    }
623                                    if needs_auto_register {
624                                        sig_trait.generics.make_where_clause();
625                                        sig_trait
626                                            .generics
627                                            .where_clause
628                                            .as_mut()
629                                            .unwrap()
630                                            .predicates
631                                            .push(syn::parse_quote!(Self: rogue_runtime::AutoRegister + Sized));
632                                    }
633
634                                    trait_items.push(quote! { #sig_trait ; });
635
636                                    /* ------------- keep original method (for Impl type) ------------------- */
637                                    let mut m_impl = m.clone();
638                                    m_impl.vis = syn::Visibility::Inherited;
639                                    impl_items.push(m_impl.into_token_stream());
640
641                                    /* ------------- build ObjectRef stub method ---------------------------- */
642                                    let sig_stub = m.sig.clone();
643
644                                    let mut fld_inits = Vec::<TokenStream2>::new();
645                                    let mut has_receiver = false;
646                                    for arg in &m.sig.inputs {
647                                        match arg {
648                                            syn::FnArg::Receiver(_) => {
649                                                has_receiver = true;
650                                            }
651                                            syn::FnArg::Typed(pt) => {
652                                                if let syn::Pat::Ident(pat_ident) = &*pt.pat {
653                                                    let nm = &pat_ident.ident;
654                                                    fld_inits.push(quote! { #nm: #nm });
655                                                }
656                                            }
657                                        }
658                                    }
659                                    if has_receiver {
660                                        fld_inits.insert(0, quote! { __instance_id: *self.id() });
661                                    }
662
663                                    let pascal = m.sig.ident.to_string().to_pascal_case();
664                                    let struct_prefix = self_ident.to_string();
665                                    let handler_ident = Ident::new(
666                                        &format!("{}{}Handler", struct_prefix, pascal),
667                                        Span::call_site(),
668                                    );
669                                    let input_ident = Ident::new(
670                                        &format!("{}{}Input", struct_prefix, pascal),
671                                        Span::call_site(),
672                                    );
673
674                                    let ret_ty_tokens: TokenStream2 = match &m.sig.output {
675                                        syn::ReturnType::Type(_, ty) => quote! { #ty },
676                                        syn::ReturnType::Default => quote! { () },
677                                    };
678
679                                    let target_expr = if has_receiver {
680                                        quote! { self.runtime_id() }
681                                    } else {
682                                        quote! { ctx.target_id.clone() }
683                                    };
684
685                                    let rpc_name = quote! { concat!(stringify!(#struct_prefix), "::", stringify!(#pascal)) };
686                                    let body_stmts = build_rpc_call_body(
687                                        &input_ident,
688                                        &handler_ident,
689                                        &fld_inits,
690                                        quote! { #target_expr },
691                                        rpc_name,
692                                        quote! { #ret_ty_tokens },
693                                    );
694
695                                    stub_items.push(quote! {
696                                        #sig_stub {
697                                            #body_stmts
698                                        }
699                                    });
700
701                                    /* ------------- server‑side handler generation ------------------------- */
702                                    let handler_ts = generate_message_handler(
703                                        &m.sig.ident,
704                                        Some(&impl_ident),
705                                        &m.sig.inputs,
706                                        &m.sig.output,
707                                    );
708                                    extra.push(handler_ts);
709                                }
710                            }
711
712                            let stub_ty: syn::Type =
713                                syn::parse_quote!(rogue_runtime::ObjectRef<#impl_ident>);
714
715                            if let Some((_, trait_path, _)) = &imp.trait_ {
716                                /* ------ user wrote `impl SomeTrait for Type` --------------------------- */
717                                let trait_block = quote! {
718                                    #[rogue_runtime::async_trait]
719                                    impl #trait_path for #stub_ty {
720                                        #(#stub_items)*
721                                    }
722                                };
723                                extra.push(trait_block);
724                            } else {
725                                /* ------ inherent impl: synthesise a new trait -------------------------- */
726                                let trait_ident =
727                                    Ident::new(&format!("{}Trait", self_ident), Span::call_site());
728
729                                let trait_block = quote! {
730                                    #[rogue_runtime::async_trait]
731                                    pub trait #trait_ident {
732                                        #(#trait_items)*
733                                    }
734
735                                    #[rogue_runtime::async_trait]
736                                    impl #trait_ident for #impl_ident {
737                                        #(#impl_items)*
738                                    }
739
740                                    #[rogue_runtime::async_trait]
741                                    impl #trait_ident for #stub_ty {
742                                        #(#stub_items)*
743                                    }
744                                };
745                                extra.push(trait_block);
746                            }
747                        }
748
749                        emitted.push(item.into_token_stream());
750                    } else {
751                        // impl for unrelated type, pass through
752                        emitted.push(item.into_token_stream());
753                    }
754                } else {
755                    emitted.push(item.into_token_stream());
756                }
757            }
758
759            /* ---- traits --------------------------------------------------------------- */
760            Item::Trait(trt) => {
761                // prepend #[rogue_runtime::async_trait] unless already present
762                let has_async_trait = trt.attrs.iter().any(|a| {
763                    a.path()
764                        .segments
765                        .last()
766                        .map(|s| s.ident == "async_trait")
767                        .unwrap_or(false)
768                });
769                if !has_async_trait {
770                    trt.attrs
771                        .insert(0, syn::parse_quote!(#[rogue_runtime::async_trait]));
772                }
773                emitted.push(item.into_token_stream());
774            }
775            /* ---- everything else: pass through ---------------------------------------- */
776            _ => emitted.push(item.into_token_stream()),
777        }
778    }
779
780    /* ---------- final output ---------- */
781    emitted.extend(extra);
782
783    quote! { #(#emitted)* }.into()
784}