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) {}