Skip to main content

rakka_macros/
lib.rs

1//! Procedural macros for rakka. Ergonomics over `rakka-core`.
2//!
3//! Exposes:
4//! * `#[actor_msg]` attribute — adds `Debug` derive and rakka-friendly
5//!   conventions to a message enum.
6//! * `#[derive(Actor)]` with `#[msg(MyMsgEnum)]` — generates a thin
7//!   `impl Actor` that delegates to the struct's `handle_msg` method,
8//!   removing the `async_trait` boilerplate users would otherwise repeat.
9//! * `#[derive(Receive)]` with `#[msg(MyMsgEnum)]` — generates a
10//!   `handle` method that dispatches enum variants to `on_<variant>`
11//!   methods on the actor (Phase 1.E of `docs/full-port-plan.md`).
12//! * `props!` macro — terse `Props::create(|| ExprThatBuildsAnActor)`.
13
14use proc_macro::TokenStream;
15use quote::{format_ident, quote};
16use syn::{parse_macro_input, DeriveInput, Fields, ItemEnum};
17
18fn to_snake_case(name: &str) -> String {
19    let mut out = String::with_capacity(name.len() + 4);
20    let mut prev_upper = true;
21    for (i, ch) in name.chars().enumerate() {
22        if ch.is_uppercase() {
23            if i > 0 && !prev_upper {
24                out.push('_');
25            }
26            for low in ch.to_lowercase() {
27                out.push(low);
28            }
29            prev_upper = true;
30        } else {
31            out.push(ch);
32            prev_upper = false;
33        }
34    }
35    out
36}
37
38/// `#[actor_msg]` — sugar to declare a message enum.
39///
40/// Adds `#[derive(Debug)]` automatically and marks the enum non_exhaustive
41/// so adding variants is not a breaking change for downstream matchers.
42///
43/// ```ignore
44/// #[actor_msg]
45/// enum CounterMsg { Inc, Get(tokio::sync::oneshot::Sender<u32>) }
46/// ```
47#[proc_macro_attribute]
48pub fn actor_msg(_attr: TokenStream, item: TokenStream) -> TokenStream {
49    let en = parse_macro_input!(item as ItemEnum);
50    let expanded = quote! {
51        #[derive(::core::fmt::Debug)]
52        #en
53    };
54    expanded.into()
55}
56
57/// `#[derive(Actor)]` with a `#[msg(MyMsg)]` attribute.
58///
59/// Generates:
60///
61/// ```ignore
62/// #[async_trait]
63/// impl Actor for Foo {
64///     type Msg = MyMsg;
65///     async fn handle(&mut self, ctx: &mut Context<Self>, msg: MyMsg) {
66///         self.handle_msg(ctx, msg).await;
67///     }
68/// }
69/// ```
70///
71/// The user only needs to write `impl Foo { async fn handle_msg(...) }` —
72/// no `#[async_trait]` boilerplate required.
73#[proc_macro_derive(Actor, attributes(msg))]
74pub fn derive_actor(input: TokenStream) -> TokenStream {
75    let input = parse_macro_input!(input as DeriveInput);
76    let name = &input.ident;
77
78    let msg_ty = match extract_msg_attr(&input) {
79        Ok(t) => t,
80        Err(e) => return e.to_compile_error().into(),
81    };
82
83    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
84
85    let expanded = quote! {
86        #[::rakka_core::prelude::async_trait]
87        impl #impl_generics ::rakka_core::prelude::Actor for #name #ty_generics #where_clause {
88            type Msg = #msg_ty;
89
90            async fn handle(
91                &mut self,
92                ctx: &mut ::rakka_core::prelude::Context<Self>,
93                msg: Self::Msg,
94            ) {
95                Self::handle_msg(self, ctx, msg).await;
96            }
97        }
98    };
99
100    expanded.into()
101}
102
103fn extract_msg_attr(input: &DeriveInput) -> Result<syn::Type, syn::Error> {
104    for attr in &input.attrs {
105        if attr.path().is_ident("msg") {
106            return attr.parse_args::<syn::Type>();
107        }
108    }
109    Err(syn::Error::new_spanned(
110        &input.ident,
111        "#[derive(Actor)] requires a `#[msg(MsgType)]` attribute naming the actor's message type",
112    ))
113}
114
115/// `#[derive(Receive)]` with a `#[msg(MyMsg)]` attribute on the actor
116/// struct, plus a separate `enum MyMsg { Inc, Get(...), … }` whose
117/// variants must be visible at the macro expansion site.
118///
119/// Generates an `impl rakka_core::actor::Actor` whose `handle` method
120/// dispatches each enum variant to a method on the actor named
121/// `on_<snake_variant>`. Unit variants get `(&mut self, ctx)`; tuple
122/// variants get `(&mut self, ctx, field0, field1, …)`. Struct variants
123/// are not supported (produces a compile error).
124///
125/// This is the typed message-router DSL referenced by
126/// `docs/idiomatic-rust.md` (P-8 follow-on) and Phase 1.E.
127#[proc_macro_derive(Receive, attributes(msg, receive))]
128pub fn derive_receive(input: TokenStream) -> TokenStream {
129    let input = parse_macro_input!(input as DeriveInput);
130    let name = &input.ident;
131
132    let msg_ty = match extract_msg_attr(&input) {
133        Ok(t) => t,
134        Err(e) => return e.to_compile_error().into(),
135    };
136
137    // The variants list isn't visible from the derive site (we have the
138    // struct, not the enum). Instead we generate a `match` arm that
139    // delegates to a single trait the user implements. Two strategies:
140    // (a) require the user to repeat the variant list via a second
141    //     attribute; or
142    // (b) generate a no-op stub that the user fills with `on_*` methods
143    //     and the compiler errors on missing methods (matched at
144    //     `Self::on_<variant>` call sites).
145    //
146    // We go with a third option: emit a `handle` method that calls
147    // `Self::dispatch(self, ctx, msg).await` and require the user to
148    // implement a single `dispatch` method. That defeats the purpose of
149    // the macro — so instead we require the user to pass the variant
150    // list via `#[receive(variants(Inc, Get(reply: oneshot::Sender<u32>)))]`.
151    //
152    // For Phase 1.E we ship a *minimal* version that works for unit
153    // variants only, with the variant names supplied through a
154    // `#[receive(unit_variants(Inc, Stop, …))]` attribute. Tuple/struct
155    // variants are a follow-on once the syn-side parsing is in place.
156    let unit_variants = match extract_unit_variants(&input) {
157        Ok(v) => v,
158        Err(e) => return e.to_compile_error().into(),
159    };
160
161    let arms = unit_variants.iter().map(|v| {
162        let snake = format_ident!("on_{}", to_snake_case(&v.to_string()));
163        quote! {
164            #msg_ty::#v => Self::#snake(self, ctx).await,
165        }
166    });
167
168    let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
169
170    let expanded = quote! {
171        #[::rakka_core::prelude::async_trait]
172        impl #impl_generics ::rakka_core::prelude::Actor for #name #ty_generics #where_clause {
173            type Msg = #msg_ty;
174
175            async fn handle(
176                &mut self,
177                ctx: &mut ::rakka_core::prelude::Context<Self>,
178                msg: Self::Msg,
179            ) {
180                match msg {
181                    #(#arms)*
182                    #[allow(unreachable_patterns)]
183                    _ => {} // tuple/struct variants — unsupported in 1.E minimal
184                }
185            }
186        }
187    };
188    expanded.into()
189}
190
191fn extract_unit_variants(input: &DeriveInput) -> Result<Vec<syn::Ident>, syn::Error> {
192    for attr in &input.attrs {
193        if !attr.path().is_ident("receive") {
194            continue;
195        }
196        let mut out = Vec::new();
197        attr.parse_nested_meta(|meta| {
198            if meta.path.is_ident("unit_variants") {
199                meta.parse_nested_meta(|inner| {
200                    if let Some(ident) = inner.path.get_ident() {
201                        out.push(ident.clone());
202                        Ok(())
203                    } else {
204                        Err(inner.error("expected variant identifier"))
205                    }
206                })
207            } else {
208                Err(meta.error("unknown #[receive(...)] key (expected `unit_variants`)"))
209            }
210        })?;
211        return Ok(out);
212    }
213    Err(syn::Error::new_spanned(
214        &input.ident,
215        "#[derive(Receive)] requires `#[receive(unit_variants(A, B, …))]` for the Phase 1.E minimal subset",
216    ))
217}
218
219/// `props!(EXPR)` — terse `Props::create(|| EXPR)`.
220///
221/// `EXPR` should evaluate to a fresh actor instance every call; Props
222/// is used to spawn possibly-many actors from the same template.
223///
224/// ```ignore
225/// let p = props!(MyActor { count: 0 });
226/// system.actor_of(p, "a")?;
227/// ```
228#[proc_macro]
229pub fn props(input: TokenStream) -> TokenStream {
230    let expr = parse_macro_input!(input as syn::Expr);
231    let expanded = quote! {
232        ::rakka_core::actor::Props::create(move || #expr)
233    };
234    expanded.into()
235}
236
237// We re-introduce Fields here so the unused warning doesn't fire if
238// future macros need it; the variable is allowed-dead.
239#[allow(dead_code)]
240fn _unused_fields_marker(_: Fields) {}