Skip to main content

behavior_macros/
lib.rs

1//! `behavior-macros` — proc-macros for the behavior algebra.
2//!
3//! `workers!` compiles a mixed fleet declaration into the erasure-free sum
4//! a `Supervisor` fleet requires (design: actorpass docs, surface talk #2):
5//! `(count, Type, build_fn)` per worker kind → a `Worker` enum with a
6//! delegated `Behavior` impl, a per-variant range `build_worker`, and the
7//! total count. `Worker` is a type—every worker stays its own actor.
8//!
9//! v1 scope: every worker kind shares the SAME protocol (`Event`, `Sends`,
10//! `Error`, and `Birth` — taken from the first kind). Mixed
11//! protocols need the hand-written sum (the `WorkerMsg` widening is a
12//! deliberate, documented step — not this macro's job yet).
13
14use proc_macro::TokenStream;
15use proc_macro_crate::{FoundCrate, crate_name};
16use proc_macro2::Span;
17use quote::{format_ident, quote};
18use syn::parse::{Parse, ParseStream};
19use syn::punctuated::Punctuated;
20use syn::{
21    Error, Expr, FnArg, ImplItem, ItemImpl, LitInt, Result, ReturnType, Token, Type,
22    parse_macro_input,
23};
24
25mod behavior_kw {
26    syn::custom_keyword!(addr);
27    syn::custom_keyword!(message);
28    syn::custom_keyword!(sends);
29    syn::custom_keyword!(births);
30    syn::custom_keyword!(error);
31}
32
33fn behavior_crate() -> Result<proc_macro2::TokenStream> {
34    if std::env::var("CARGO_PKG_NAME").as_deref() == Ok("bombay-behavior") {
35        // This package deliberately exposes the library target as `behavior`,
36        // not Cargo's normalized package name `bombay_behavior`. The same path
37        // works in its unit, integration, and rustdoc crates.
38        return Ok(quote!(::behavior));
39    }
40    match crate_name("bombay-behavior") {
41        Ok(FoundCrate::Itself) => Ok(quote!(::behavior)),
42        Ok(FoundCrate::Name(name)) => {
43            let name = syn::Ident::new(&name, Span::call_site());
44            Ok(quote!(::#name))
45        }
46        Err(error) => Err(Error::new(
47            Span::call_site(),
48            format!("could not resolve the bombay-behavior crate: {error}"),
49        )),
50    }
51}
52
53struct BehaviorArgs {
54    addr: Type,
55    message: Type,
56    sends: Type,
57    births: Type,
58    error: Type,
59}
60
61impl Parse for BehaviorArgs {
62    fn parse(input: ParseStream) -> Result<Self> {
63        input.parse::<behavior_kw::addr>()?;
64        input.parse::<Token![=]>()?;
65        let addr = input.parse()?;
66        input.parse::<Token![,]>()?;
67        input.parse::<behavior_kw::message>()?;
68        input.parse::<Token![=]>()?;
69        let message = input.parse()?;
70        input.parse::<Token![,]>()?;
71        input.parse::<behavior_kw::sends>()?;
72        input.parse::<Token![=]>()?;
73        let sends = input.parse()?;
74        input.parse::<Token![,]>()?;
75        input.parse::<behavior_kw::births>()?;
76        input.parse::<Token![=]>()?;
77        let births = input.parse()?;
78        input.parse::<Token![,]>()?;
79        input.parse::<behavior_kw::error>()?;
80        input.parse::<Token![=]>()?;
81        let error = input.parse()?;
82        if input.peek(Token![,]) {
83            input.parse::<Token![,]>()?;
84        }
85        if !input.is_empty() {
86            return Err(input
87                .error("expected exactly addr, message, sends, births, and error in that order"));
88        }
89        Ok(Self {
90            addr,
91            message,
92            sends,
93            births,
94            error,
95        })
96    }
97}
98
99fn validate_receiver(method: &syn::ImplItemFn) -> Result<()> {
100    let Some(FnArg::Receiver(receiver)) = method.sig.inputs.first() else {
101        return Err(Error::new_spanned(
102            &method.sig,
103            "behavior methods must begin with &mut self",
104        ));
105    };
106    if !matches!(receiver.kind, syn::ReceiverKind::Reference(_, _, Some(_))) {
107        return Err(Error::new_spanned(
108            receiver,
109            "behavior methods must begin with &mut self",
110        ));
111    }
112    if method.sig.constness.is_some()
113        || method.sig.asyncness.is_some()
114        || matches!(method.sig.safety, syn::Safety::Unsafe(_))
115        || !method.sig.generics.params.is_empty()
116    {
117        return Err(Error::new_spanned(
118            &method.sig,
119            "behavior init and receive methods must be synchronous, safe, and non-generic",
120        ));
121    }
122    if matches!(method.sig.output, ReturnType::Default) {
123        return Err(Error::new_spanned(
124            &method.sig,
125            "behavior methods must declare their complete Actions result type",
126        ));
127    }
128    Ok(())
129}
130
131/// Generate the mechanical `Behavior` implementation for a normal inherent
132/// impl containing `receive(&mut self, from, message)` and, optionally,
133/// `init(&mut self)`. Omitting `init` selects the behavior algebra's empty
134/// initialization transition. The original impl and methods are preserved
135/// unchanged.
136#[proc_macro_attribute]
137pub fn behavior(args: TokenStream, item: TokenStream) -> TokenStream {
138    let args = parse_macro_input!(args as BehaviorArgs);
139    let item = parse_macro_input!(item as ItemImpl);
140
141    if item.trait_.is_some() {
142        return Error::new_spanned(
143            &item,
144            "#[behavior] applies to an inherent impl, not a trait impl",
145        )
146        .to_compile_error()
147        .into();
148    }
149
150    let init = item.items.iter().find_map(|item| match item {
151        ImplItem::Fn(method) if method.sig.ident == "init" => Some(method),
152        _ => None,
153    });
154    let receive = item.items.iter().find_map(|item| match item {
155        ImplItem::Fn(method) if method.sig.ident == "receive" => Some(method),
156        _ => None,
157    });
158    let Some(receive) = receive else {
159        return Error::new_spanned(
160            &item.self_ty,
161            "#[behavior] requires a receive(&mut self, from, message) method",
162        )
163        .to_compile_error()
164        .into();
165    };
166    if let Err(error) = init
167        .map_or(Ok(()), validate_receiver)
168        .and_then(|()| validate_receiver(receive))
169    {
170        return error.to_compile_error().into();
171    }
172    if let Some(init) = init
173        && init.sig.inputs.len() != 1
174    {
175        return Error::new_spanned(&init.sig, "init must accept exactly &mut self")
176            .to_compile_error()
177            .into();
178    }
179    if receive.sig.inputs.len() != 3 {
180        return Error::new_spanned(
181            &receive.sig,
182            "receive must accept exactly &mut self, from, and message",
183        )
184        .to_compile_error()
185        .into();
186    }
187
188    let BehaviorArgs {
189        addr,
190        message,
191        sends,
192        births,
193        error,
194    } = args;
195    let self_ty = &item.self_ty;
196    let (impl_generics, _, where_clause) = item.generics.split_for_impl();
197    let behavior = match behavior_crate() {
198        Ok(behavior) => behavior,
199        Err(error) => return error.to_compile_error().into(),
200    };
201    let initialize = init.map_or_else(
202        || quote!(::core::result::Result::Ok(#behavior::Actions::cont())),
203        |_| quote!(<#self_ty>::init(self)),
204    );
205
206    quote! {
207        #item
208
209        impl #impl_generics #behavior::Behavior for #self_ty #where_clause {
210            type Addr = #addr;
211            type Msg = #message;
212            type Event = #behavior::User<#addr, #message>;
213            type Sends = #sends;
214            type Ph = #behavior::Never;
215            type Error = #error;
216            type Birth = #births;
217
218            fn init(
219                &mut self,
220                _: #behavior::InitializationTurn,
221            ) -> #behavior::BehaviorActed<Self> {
222                #initialize
223            }
224
225            fn transition(
226                &mut self,
227                _: #behavior::ActiveTurn,
228                event: Self::Event,
229            ) -> #behavior::BehaviorActed<Self> {
230                <#self_ty>::receive(self, event.from, event.message)
231            }
232        }
233
234        impl #impl_generics #behavior::BehaviorBase for #self_ty #where_clause {
235            type Base = Self;
236
237            fn base(&self) -> &Self {
238                self
239            }
240        }
241    }
242    .into()
243}
244
245/// One `(count, Type, build_fn)` worker-kind spec.
246struct Compose {
247    count: LitInt,
248    ty: Type,
249    build: Expr,
250}
251
252impl Parse for Compose {
253    fn parse(input: ParseStream) -> Result<Self> {
254        let content;
255        syn::parenthesized!(content in input);
256        let count: Expr = content.parse()?;
257        let Expr::Lit(syn::ExprLit {
258            lit: syn::Lit::Int(count),
259            ..
260        }) = count
261        else {
262            return Err(Error::new_spanned(
263                count,
264                "worker count must be a usize literal (ranges are computed at expansion)",
265            ));
266        };
267        content.parse::<Token![,]>()?;
268        let ty: Type = content.parse()?;
269        content.parse::<Token![,]>()?;
270        let build: Expr = content.parse()?;
271        Ok(Compose { count, ty, build })
272    }
273}
274
275struct Specs(Punctuated<Compose, Token![,]>);
276
277impl Parse for Specs {
278    fn parse(input: ParseStream) -> Result<Self> {
279        Ok(Specs(Punctuated::parse_terminated(input)?))
280    }
281}
282
283/// `workers![(4, WorkerA, build_a), (2, WorkerB, build_b)]` → a block
284/// declaring the `Worker` sum and yielding `(total, build_worker)` for
285/// `Supervisor`'s fleet. Slots are contiguous per variant (slot = nonce;
286/// rest-for-one's birth order is the declaration order).
287#[proc_macro]
288pub fn workers(input: TokenStream) -> TokenStream {
289    let Specs(specs) = parse_macro_input!(input as Specs);
290    let specs: Vec<Compose> = specs.into_iter().collect();
291    if specs.is_empty() {
292        return Error::new(
293            proc_macro2::Span::call_site(),
294            "workers! needs at least one (count, Type, build_fn) spec",
295        )
296        .to_compile_error()
297        .into();
298    }
299
300    let first_ty = &specs[0].ty;
301    let behavior = match behavior_crate() {
302        Ok(behavior) => behavior,
303        Err(error) => return error.to_compile_error().into(),
304    };
305    let variants: Vec<_> = specs
306        .iter()
307        .enumerate()
308        .map(|(i, _)| format_ident!("V{i}"))
309        .collect();
310    let variant_defs = specs.iter().zip(&variants).map(|(s, v)| {
311        let ty = &s.ty;
312        quote! { #v(#ty) }
313    });
314
315    let mut start = 0_usize;
316    let mut build_arms = Vec::new();
317    for (s, v) in specs.iter().zip(&variants) {
318        let n: usize = match s.count.base10_parse() {
319            Ok(n) => n,
320            Err(e) => return e.to_compile_error().into(),
321        };
322        let end = start + n;
323        let build = &s.build;
324        build_arms
325            .push(quote! { #start..#end => ::core::option::Option::Some(Worker::#v((#build)(i))) });
326        start = end;
327    }
328    let total = start;
329
330    let step_arms = variants
331        .iter()
332        .map(|v| quote! { Worker::#v(b) => b.transition(turn, ev) });
333    let init_arms = variants
334        .iter()
335        .map(|v| quote! { Worker::#v(b) => b.init(turn) });
336
337    let out = quote! {
338        {
339            /// The macro-generated mixed-fleet sum (see `workers!`).
340            enum Worker {
341                #(#variant_defs),*
342            }
343
344            impl #behavior::Behavior for Worker {
345                type Addr = <#first_ty as #behavior::Behavior>::Addr;
346                type Msg = <#first_ty as #behavior::Behavior>::Msg;
347                type Event = <#first_ty as #behavior::Behavior>::Event;
348                type Sends = <#first_ty as #behavior::Behavior>::Sends;
349                type Ph = #behavior::Never;
350                type Error = <#first_ty as #behavior::Behavior>::Error;
351                type Birth = <#first_ty as #behavior::Behavior>::Birth;
352
353                fn init(&mut self, turn: #behavior::InitializationTurn) -> ::core::result::Result<
354                    #behavior::Actions<Self::Addr, Self::Ph, Self::Sends, Self::Birth>,
355                    Self::Error,
356                > {
357                    match self {
358                        #(#init_arms),*
359                    }
360                }
361
362                fn transition(
363                    &mut self,
364                    turn: #behavior::ActiveTurn,
365                    ev: Self::Event,
366                ) -> ::core::result::Result<
367                    #behavior::Actions<Self::Addr, Self::Ph, Self::Sends, Self::Birth>,
368                    Self::Error,
369                > {
370                    match self {
371                        #(#step_arms),*
372                    }
373                }
374
375            }
376
377            fn build_worker(i: usize) -> ::core::option::Option<Worker> {
378                match i {
379                    #(#build_arms,)*
380                    _ => ::core::option::Option::None,
381                }
382            }
383
384            (#total, build_worker as fn(usize) -> ::core::option::Option<Worker>)
385        }
386    };
387    out.into()
388}