Skip to main content

behavior_macros/
lib.rs

1//! `behavior-macros` — proc-macros for the behavior algebra.
2//!
3//! `#[behavior]` wires a fully declared inherent user-message fold and may
4//! generate its nominal send and closed birth products.
5
6use proc_macro::TokenStream;
7use proc_macro_crate::{FoundCrate, crate_name};
8use proc_macro2::{Span, TokenStream as TokenStream2};
9use quote::{format_ident, quote};
10use syn::parse::{Parse, ParseStream};
11use syn::visit::Visit;
12use syn::{
13    Error, FnArg, GenericArgument, GenericParam, Generics, Ident, ImplItem, ItemImpl,
14    PathArguments, Result, ReturnType, Token, Type, braced, parse_macro_input, parse_quote,
15};
16
17fn crate_path(found: FoundCrate) -> TokenStream2 {
18    match found {
19        FoundCrate::Itself => quote!(crate),
20        FoundCrate::Name(name) => {
21            let name = syn::Ident::new(&name, Span::call_site());
22            quote!(::#name)
23        }
24    }
25}
26
27fn facade_crate_path(found: FoundCrate) -> TokenStream2 {
28    match found {
29        FoundCrate::Itself => quote!(crate),
30        FoundCrate::Name(name) => {
31            // Cargo names the package `bombay-rs`, while its public library
32            // target is `bombay`. `proc_macro_crate` reports the normalized
33            // package name for an unrenamed dependency, but generated Rust
34            // must address the library target. An explicit dependency rename
35            // remains the caller's actual extern-crate name.
36            let name = if name == "bombay_rs" {
37                "bombay".to_owned()
38            } else {
39                name
40            };
41            let name = syn::Ident::new(&name, Span::call_site());
42            quote!(::#name)
43        }
44    }
45}
46
47fn behavior_crate() -> Result<TokenStream2> {
48    if std::env::var("CARGO_PKG_NAME").as_deref() == Ok("bombay-behavior") {
49        // This package deliberately exposes the library target as `behavior`,
50        // not Cargo's normalized package name `bombay_behavior`. The same path
51        // works in its unit, integration, and rustdoc crates.
52        return Ok(quote!(::behavior));
53    }
54    if std::env::var("CARGO_PKG_NAME").as_deref() == Ok("bombay-rs") {
55        // `FoundCrate::Itself` identifies a package, but `crate` identifies
56        // the target currently being compiled. The facade's library target is
57        // `bombay`; sibling binaries and examples therefore reach its exports
58        // through `::bombay`, not through their own `crate` root.
59        return if std::env::var("CARGO_CRATE_NAME").as_deref() == Ok("bombay") {
60            Ok(quote!(crate::behavior))
61        } else {
62            Ok(quote!(::bombay::behavior))
63        };
64    }
65    if let Ok(found) = crate_name("bombay-behavior") {
66        return Ok(crate_path(found));
67    }
68    if let Ok(found) = crate_name("bombay-rs") {
69        let bombay = facade_crate_path(found);
70        return Ok(quote!(#bombay::behavior));
71    }
72    Err(Error::new(
73        Span::call_site(),
74        "could not resolve `bombay-behavior` directly or through `bombay-rs`",
75    ))
76}
77
78fn actors_crate() -> Result<TokenStream2> {
79    if std::env::var("CARGO_PKG_NAME").as_deref() == Ok("bombay-behavior-actors") {
80        return if std::env::var("CARGO_CRATE_NAME").as_deref() == Ok("behavior_actors") {
81            Ok(quote!(crate))
82        } else {
83            Ok(quote!(::behavior_actors))
84        };
85    }
86    if std::env::var("CARGO_PKG_NAME").as_deref() == Ok("bombay-rs") {
87        return if std::env::var("CARGO_CRATE_NAME").as_deref() == Ok("bombay") {
88            Ok(quote!(crate))
89        } else {
90            Ok(quote!(::bombay))
91        };
92    }
93    if let Ok(found) = crate_name("bombay-behavior-actors") {
94        return Ok(crate_path(found));
95    }
96    if let Ok(found) = crate_name("bombay-rs") {
97        return Ok(facade_crate_path(found));
98    }
99    Err(Error::new(
100        Span::call_site(),
101        "could not resolve `bombay-behavior-actors` directly or through `bombay-rs`",
102    ))
103}
104
105#[cfg(test)]
106mod crate_resolution_tests {
107    use super::*;
108
109    #[test]
110    fn facade_default_library_name_and_explicit_rename_resolve_distinctly() {
111        assert_eq!(
112            facade_crate_path(FoundCrate::Name("bombay_rs".to_owned())).to_string(),
113            ":: bombay"
114        );
115        assert_eq!(
116            facade_crate_path(FoundCrate::Name("runtime".to_owned())).to_string(),
117            ":: runtime"
118        );
119    }
120}
121
122struct NamedField {
123    name: Ident,
124    ty: Type,
125}
126
127struct NamedProduct {
128    fields: Vec<NamedField>,
129}
130
131#[allow(
132    clippy::large_enum_variant,
133    reason = "boxing syn::Type would add lint-driven indirection to compile-time parser state"
134)]
135enum SendsSpec {
136    Existing(Type),
137    Generated(NamedProduct),
138}
139
140#[allow(
141    clippy::large_enum_variant,
142    reason = "boxing syn::Type would add lint-driven indirection to compile-time parser state"
143)]
144enum BirthsSpec {
145    Existing(Type),
146    Generated(NamedProduct),
147}
148
149impl BirthsSpec {
150    fn is_no_births(&self) -> bool {
151        let Self::Existing(Type::Path(path)) = self else {
152            return false;
153        };
154        path.qself.is_none()
155            && path.path.segments.last().is_some_and(|segment| {
156                segment.ident == "NoBirths" && matches!(segment.arguments, syn::PathArguments::None)
157            })
158    }
159}
160
161#[derive(Clone, Copy)]
162enum CreationSettlementDisposition {
163    ReturnToCreator,
164    RetainForRetirement,
165}
166
167fn parse_product(input: ParseStream) -> Result<NamedProduct> {
168    let content;
169    braced!(content in input);
170    let mut fields = Vec::new();
171    let mut names = std::collections::BTreeSet::new();
172    while !content.is_empty() {
173        let name: Ident = content.parse()?;
174        if !names.insert(name.to_string()) {
175            return Err(Error::new_spanned(name, "duplicate product lane"));
176        }
177        content.parse::<Token![:]>()?;
178        fields.push(NamedField {
179            name,
180            ty: content.parse()?,
181        });
182        if content.peek(Token![,]) {
183            content.parse::<Token![,]>()?;
184        } else if !content.is_empty() {
185            return Err(content.error("expected `,` between product lanes"));
186        }
187    }
188    if fields.is_empty() {
189        return Err(input.error("empty products must be omitted"));
190    }
191    Ok(NamedProduct { fields })
192}
193
194struct BehaviorArgs {
195    addr: Type,
196    message: Type,
197    sends: Option<SendsSpec>,
198    births: Option<BirthsSpec>,
199    creation_settlements: Option<CreationSettlementDisposition>,
200    error: Option<Type>,
201}
202
203struct PoolWorkerArgs {
204    addr: Type,
205    result: Type,
206}
207
208impl Parse for PoolWorkerArgs {
209    fn parse(input: ParseStream) -> Result<Self> {
210        let mut addr = None;
211        let mut result = None;
212        while !input.is_empty() {
213            let key: Ident = input.parse()?;
214            input.parse::<Token![=]>()?;
215            match key.to_string().as_str() {
216                "addr" if addr.is_none() => addr = Some(input.parse()?),
217                "result" if result.is_none() => result = Some(input.parse()?),
218                "addr" | "result" => {
219                    return Err(Error::new_spanned(key, "duplicate pool worker argument"));
220                }
221                _ => return Err(Error::new_spanned(key, "unknown pool worker argument")),
222            }
223            if input.peek(Token![,]) {
224                input.parse::<Token![,]>()?;
225            } else if !input.is_empty() {
226                return Err(input.error("expected `,` between pool worker arguments"));
227            }
228        }
229        Ok(Self {
230            addr: addr.ok_or_else(|| input.error("missing `addr`"))?,
231            result: result.ok_or_else(|| input.error("missing `result`"))?,
232        })
233    }
234}
235
236impl Parse for BehaviorArgs {
237    fn parse(input: ParseStream) -> Result<Self> {
238        let mut addr = None;
239        let mut message = None;
240        let mut sends = None;
241        let mut births = None;
242        let mut creation_settlements = None;
243        let mut error = None;
244        while !input.is_empty() {
245            let key: Ident = input.parse()?;
246            input.parse::<Token![=]>()?;
247            match key.to_string().as_str() {
248                "addr" if addr.is_none() => addr = Some(input.parse()?),
249                "message" if message.is_none() => message = Some(input.parse()?),
250                "sends" if sends.is_none() => {
251                    sends = Some(if input.peek(syn::token::Brace) {
252                        SendsSpec::Generated(parse_product(input)?)
253                    } else {
254                        SendsSpec::Existing(input.parse()?)
255                    });
256                }
257                "births" if births.is_none() => {
258                    births = Some(if input.peek(syn::token::Brace) {
259                        BirthsSpec::Generated(parse_product(input)?)
260                    } else {
261                        BirthsSpec::Existing(input.parse()?)
262                    });
263                }
264                "creation_settlements" if creation_settlements.is_none() => {
265                    let disposition: Ident = input.parse()?;
266                    creation_settlements = Some(match disposition.to_string().as_str() {
267                        "return_to_creator" => CreationSettlementDisposition::ReturnToCreator,
268                        "retain_for_retirement" => {
269                            CreationSettlementDisposition::RetainForRetirement
270                        }
271                        _ => {
272                            return Err(Error::new_spanned(
273                                disposition,
274                                "creation settlements must `return_to_creator` or `retain_for_retirement`",
275                            ));
276                        }
277                    });
278                }
279                "error" if error.is_none() => error = Some(input.parse()?),
280                "addr" | "message" | "sends" | "births" | "creation_settlements" | "error" => {
281                    return Err(Error::new_spanned(key, "duplicate behavior argument"));
282                }
283                _ => return Err(Error::new_spanned(key, "unknown behavior argument")),
284            }
285            if input.peek(Token![,]) {
286                input.parse::<Token![,]>()?;
287            } else if !input.is_empty() {
288                return Err(input.error("expected `,` between behavior arguments"));
289            }
290        }
291        match (&births, creation_settlements) {
292            (Some(births), None) if !births.is_no_births() => {
293                return Err(input.error(
294                    "birth-owning behaviors must declare `creation_settlements = return_to_creator` or `creation_settlements = retain_for_retirement`",
295                ));
296            }
297            (None, Some(_)) => {
298                return Err(input.error("`creation_settlements` requires a `births` declaration"));
299            }
300            (Some(_), Some(_)) | (Some(_), None) | (None, None) => {}
301        }
302        Ok(Self {
303            addr: addr.ok_or_else(|| input.error("missing `addr`"))?,
304            message: message.ok_or_else(|| input.error("missing `message`"))?,
305            sends,
306            births,
307            creation_settlements,
308            error,
309        })
310    }
311}
312
313fn validate_receiver(method: &syn::ImplItemFn) -> Result<()> {
314    let Some(FnArg::Receiver(receiver)) = method.sig.inputs.first() else {
315        return Err(Error::new_spanned(
316            &method.sig,
317            "behavior methods must begin with &mut self",
318        ));
319    };
320    if !matches!(receiver.kind, syn::ReceiverKind::Reference(_, _, Some(_))) {
321        return Err(Error::new_spanned(
322            receiver,
323            "behavior methods must begin with &mut self",
324        ));
325    }
326    if method.sig.constness.is_some()
327        || method.sig.asyncness.is_some()
328        || matches!(method.sig.safety, syn::Safety::Unsafe(_))
329        || !method.sig.generics.params.is_empty()
330    {
331        return Err(Error::new_spanned(
332            &method.sig,
333            "behavior methods must be synchronous, safe, and non-generic",
334        ));
335    }
336    if matches!(method.sig.output, ReturnType::Default) {
337        return Err(Error::new_spanned(
338            &method.sig,
339            "behavior methods must declare their complete Actions result type",
340        ));
341    }
342    Ok(())
343}
344
345fn validate_pool_worker_output(output: &ReturnType) -> Result<()> {
346    let ReturnType::Type(_, output) = output else {
347        return Err(Error::new_spanned(
348            output,
349            "pool worker transition must return WorkerActed<Self>",
350        ));
351    };
352    let Type::Path(output) = output.as_ref() else {
353        return Err(Error::new_spanned(
354            output,
355            "pool worker transition must return WorkerActed<Self>",
356        ));
357    };
358    if output.qself.is_some() || output.path.segments.len() != 1 {
359        return Err(Error::new_spanned(
360            output,
361            "pool worker transition must return WorkerActed<Self>",
362        ));
363    }
364    let segment = output
365        .path
366        .segments
367        .first()
368        .ok_or_else(|| Error::new_spanned(output, "missing pool worker return type"))?;
369    let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
370        return Err(Error::new_spanned(
371            output,
372            "pool worker transition must return WorkerActed<Self>",
373        ));
374    };
375    let mut arguments = arguments.args.iter();
376    let Some(GenericArgument::Type(Type::Path(self_type))) = arguments.next() else {
377        return Err(Error::new_spanned(
378            output,
379            "pool worker transition must return WorkerActed<Self>",
380        ));
381    };
382    let self_segment = self_type.path.segments.first();
383    if segment.ident != "WorkerActed"
384        || arguments.next().is_some()
385        || self_type.qself.is_some()
386        || self_type.path.segments.len() != 1
387        || !matches!(self_segment, Some(segment) if segment.ident == "Self" && matches!(segment.arguments, PathArguments::None))
388    {
389        return Err(Error::new_spanned(
390            output,
391            "pool worker transition must return WorkerActed<Self>",
392        ));
393    }
394    Ok(())
395}
396
397fn pool_worker_assignment(method: &syn::ImplItemFn) -> Result<Type> {
398    let Some(FnArg::Receiver(receiver)) = method.sig.inputs.first() else {
399        return Err(Error::new_spanned(
400            &method.sig,
401            "pool worker transition must begin with &mut self",
402        ));
403    };
404    if !matches!(receiver.kind, syn::ReceiverKind::Reference(_, _, Some(_))) {
405        return Err(Error::new_spanned(
406            receiver,
407            "pool worker transition must begin with &mut self",
408        ));
409    }
410    if method.sig.constness.is_some()
411        || method.sig.asyncness.is_some()
412        || matches!(method.sig.safety, syn::Safety::Unsafe(_))
413        || method.sig.abi.is_some()
414        || method.sig.variadic.is_some()
415        || !method.sig.generics.params.is_empty()
416    {
417        return Err(Error::new_spanned(
418            &method.sig,
419            "pool worker transition must be synchronous, safe, and non-generic",
420        ));
421    }
422    if method.sig.inputs.len() != 2 {
423        return Err(Error::new_spanned(
424            &method.sig,
425            "pool worker transition must accept exactly &mut self and one Assignment<Job>",
426        ));
427    }
428    validate_pool_worker_output(&method.sig.output)?;
429
430    let Some(FnArg::Typed(assignment)) = method.sig.inputs.iter().nth(1) else {
431        return Err(Error::new_spanned(
432            &method.sig,
433            "pool worker transition must accept one Assignment<Job>",
434        ));
435    };
436    let Type::Path(assignment_path) = assignment.ty.as_ref() else {
437        return Err(Error::new_spanned(
438            &assignment.ty,
439            "pool worker transition input must be Assignment<Job>",
440        ));
441    };
442    let Some(segment) = assignment_path.path.segments.last() else {
443        return Err(Error::new_spanned(
444            &assignment.ty,
445            "pool worker transition input must be Assignment<Job>",
446        ));
447    };
448    let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
449        return Err(Error::new_spanned(
450            &assignment.ty,
451            "pool worker transition input must be Assignment<Job>",
452        ));
453    };
454    let mut arguments = arguments.args.iter();
455    if segment.ident != "Assignment"
456        || !matches!(arguments.next(), Some(GenericArgument::Type(_)))
457        || arguments.next().is_some()
458    {
459        return Err(Error::new_spanned(
460            &assignment.ty,
461            "pool worker transition input must be Assignment<Job>",
462        ));
463    }
464    Ok(assignment.ty.as_ref().clone())
465}
466
467fn pascal_name(field: &Ident) -> String {
468    field
469        .to_string()
470        .split('_')
471        .filter(|part| !part.is_empty())
472        .map(|part| {
473            let mut chars = part.chars();
474            chars.next().map_or_else(String::new, |first| {
475                first.to_uppercase().chain(chars).collect()
476            })
477        })
478        .collect::<String>()
479}
480
481fn lane_name(product: &Ident, field: &Ident) -> Ident {
482    let lane = pascal_name(field);
483    format_ident!("{}{}", product, lane)
484}
485
486#[derive(Default)]
487struct GenericUses {
488    identifiers: std::collections::BTreeSet<String>,
489    lifetimes: std::collections::BTreeSet<String>,
490}
491
492impl<'ast> Visit<'ast> for GenericUses {
493    fn visit_ident(&mut self, ident: &'ast Ident) {
494        self.identifiers.insert(ident.to_string());
495    }
496
497    fn visit_lifetime(&mut self, lifetime: &'ast syn::Lifetime) {
498        self.lifetimes.insert(lifetime.ident.to_string());
499    }
500}
501
502fn product_generics(item: &ItemImpl, fields: &[NamedField]) -> Generics {
503    let mut uses = GenericUses::default();
504    for field in fields {
505        uses.visit_type(&field.ty);
506    }
507    let mut generics = item.generics.clone();
508    generics.params = generics
509        .params
510        .into_iter()
511        .filter(|parameter| match parameter {
512            GenericParam::Lifetime(parameter) => uses
513                .lifetimes
514                .contains(&parameter.lifetime.ident.to_string()),
515            GenericParam::Type(parameter) => {
516                uses.identifiers.contains(&parameter.ident.to_string())
517            }
518            GenericParam::Const(parameter) => {
519                uses.identifiers.contains(&parameter.ident.to_string())
520            }
521        })
522        .collect();
523    generics.where_clause = None;
524    generics
525}
526
527#[allow(
528    clippy::too_many_lines,
529    reason = "the generated nominal product keeps every structural trait implementation adjacent"
530)]
531fn generate_sends(
532    product: Option<SendsSpec>,
533    actor: &Ident,
534    item: &ItemImpl,
535    behavior: &TokenStream2,
536) -> (TokenStream2, TokenStream2) {
537    let product = match product {
538        None => return (quote!(#behavior::NoSends), quote!()),
539        Some(SendsSpec::Existing(ty)) => return (quote!(#ty), quote!()),
540        Some(SendsSpec::Generated(product)) => product,
541    };
542    let name = format_ident!("{}Sends", actor);
543    let settlements_name = format_ident!("{}Settlements", actor);
544    let field_names: Vec<_> = product.fields.iter().map(|field| &field.name).collect();
545    let field_types: Vec<_> = product.fields.iter().map(|field| &field.ty).collect();
546    let lane_names: Vec<_> = field_names
547        .iter()
548        .map(|field| lane_name(&name, field))
549        .collect();
550    let actions_name = format_ident!("{}Actions", actor);
551    let action_methods: Vec<_> = field_names
552        .iter()
553        .map(|field| {
554            let field = field.to_string();
555            format_ident!("send_{}", field.trim_start_matches("r#"))
556        })
557        .collect();
558    let product_generics = product_generics(item, &product.fields);
559    let (_, type_generics, _) = product_generics.split_for_impl();
560    let generics = &product_generics;
561    let actor_type = &item.self_ty;
562    let mut action_trait_generics = product_generics.clone();
563    action_trait_generics
564        .make_where_clause()
565        .predicates
566        .push(parse_quote!(#actor_type: ::core::marker::Sized));
567
568    let action_trait_methods =
569        action_methods
570            .iter()
571            .zip(lane_names.iter())
572            .map(|(method, lane)| {
573                quote! {
574                    /// Append one value to this generated semantic send lane.
575                    #[must_use]
576                    fn #method<__BombayInput>(self, input: __BombayInput) -> Self
577                    where
578                        Self: #behavior::AppendSend<__BombayInput, #lane>;
579                }
580            });
581
582    let action_impl_methods = action_methods
583        .iter()
584        .zip(lane_names.iter())
585        .map(|(method, lane)| {
586            quote! {
587                fn #method<__BombayInput>(self, input: __BombayInput) -> Self
588                where
589                    Self: #behavior::AppendSend<__BombayInput, #lane>,
590                {
591                    <Self as #behavior::AppendSend<__BombayInput, #lane>>::append_send(
592                        self,
593                        input,
594                    )
595                }
596            }
597        });
598
599    let mut actions_impl_generics = product_generics.clone();
600    actions_impl_generics
601        .params
602        .push(parse_quote!(__BombayAddress));
603    actions_impl_generics.params.push(parse_quote!(__BombayPh));
604    actions_impl_generics
605        .params
606        .push(parse_quote!(__BombayBirth));
607    actions_impl_generics
608        .make_where_clause()
609        .predicates
610        .push(parse_quote!(__BombayAddress: #behavior::Address));
611    actions_impl_generics
612        .make_where_clause()
613        .predicates
614        .push(parse_quote!(__BombayBirth: #behavior::BirthMode));
615    actions_impl_generics
616        .make_where_clause()
617        .predicates
618        .push(parse_quote!(#actor_type: ::core::marker::Sized));
619    let (actions_impl_generics, _, actions_where_clause) = actions_impl_generics.split_for_impl();
620
621    let send_impls = lane_names
622        .iter()
623        .zip(field_names.iter())
624        .zip(field_types.iter())
625        .map(|((lane, field), field_ty)| {
626            let mut generics = product_generics.clone();
627            generics.params.push(parse_quote!(__BombayInput));
628            generics.make_where_clause().predicates.push(parse_quote!(
629                #field_ty: #behavior::SendInput<__BombayInput, #behavior::Own>
630            ));
631            let (impl_generics, _, where_clause) = generics.split_for_impl();
632            quote! {
633                impl #impl_generics #behavior::SendInput<__BombayInput, #lane>
634                    for #name #type_generics #where_clause
635                {
636                    fn emit(&mut self, input: __BombayInput) {
637                        <#field_ty as #behavior::SendInput<
638                            __BombayInput,
639                            #behavior::Own,
640                        >>::emit(&mut self.#field, input);
641                    }
642                }
643            }
644        });
645
646    let mut effects_generics = product_generics.clone();
647    for field_ty in &field_types {
648        effects_generics
649            .make_where_clause()
650            .predicates
651            .push(parse_quote!(#field_ty: #behavior::SendEffects));
652    }
653    let (effects_impl_generics, _, effects_where_clause) = effects_generics.split_for_impl();
654
655    let mut lawful_generics = product_generics.clone();
656    lawful_generics.params.push(parse_quote!(__BombayEvent));
657    for field_ty in &field_types {
658        lawful_generics
659            .make_where_clause()
660            .predicates
661            .push(parse_quote!(#field_ty: #behavior::SendsFor<__BombayEvent>));
662    }
663    let (lawful_impl_generics, _, lawful_where_clause) = lawful_generics.split_for_impl();
664
665    let mut interpret_generics = product_generics.clone();
666    interpret_generics
667        .params
668        .push(parse_quote!(__BombayInterpreter));
669    interpret_generics
670        .params
671        .push(parse_quote!(__BombayRootEvent));
672    interpret_generics.params.push(parse_quote!(__BombayPath));
673    interpret_generics
674        .make_where_clause()
675        .predicates
676        .push(parse_quote!(__BombayInterpreter: ::core::marker::Send));
677    for field_ty in &field_types {
678        interpret_generics
679            .make_where_clause()
680            .predicates
681            .push(parse_quote!(
682                #field_ty: #behavior::InterpretSends<
683                    __BombayInterpreter,
684                    __BombayRootEvent,
685                    __BombayPath,
686                >
687            ));
688    }
689    interpret_generics
690        .make_where_clause()
691        .predicates
692        .push(parse_quote!(#name #type_generics: ::core::marker::Send));
693    let (interpret_impl_generics, _, interpret_where_clause) = interpret_generics.split_for_impl();
694    let mut settlement_generics = product_generics.clone();
695    for field_ty in &field_types {
696        settlement_generics
697            .make_where_clause()
698            .predicates
699            .push(parse_quote!(#field_ty: #behavior::SendSettlements));
700    }
701    let (settlement_impl_generics, settlement_type_generics, settlement_where_clause) =
702        settlement_generics.split_for_impl();
703    let mut classification_generics = settlement_generics.clone();
704    for field_ty in &field_types {
705        classification_generics
706            .make_where_clause()
707            .predicates
708            .push(parse_quote!(
709                <#field_ty as #behavior::SendSettlements>::Settlements:
710                    #behavior::ClassifySettlement
711            ));
712    }
713    let (classification_impl_generics, classification_type_generics, classification_where_clause) =
714        classification_generics.split_for_impl();
715    let mut custody_generics = settlement_generics.clone();
716    custody_generics
717        .params
718        .push(parse_quote!(__BombaySettlementHost));
719    custody_generics
720        .params
721        .push(parse_quote!(__BombaySettlementEvent));
722    custody_generics
723        .make_where_clause()
724        .predicates
725        .push(parse_quote!(__BombaySettlementHost: ::core::marker::Send));
726    for field_ty in &field_types {
727        custody_generics
728            .make_where_clause()
729            .predicates
730            .push(parse_quote!(
731                <#field_ty as #behavior::SendSettlements>::Settlements:
732                    #behavior::SourceSettlementCustody<
733                        __BombaySettlementHost,
734                        __BombaySettlementEvent,
735                    >
736                    + ::core::marker::Send
737            ));
738    }
739    let (custody_impl_generics, _, custody_where_clause) = custody_generics.split_for_impl();
740    let mut combined_status = quote!(#behavior::SettlementStatus::Accepted);
741    for field in &field_names {
742        combined_status = quote! {
743            (#combined_status).combine(
744                #behavior::ClassifySettlement::settlement_status(&self.#field)
745            )
746        };
747    }
748
749    let mut interpretation = quote! {
750        #behavior::Interpretation::Complete(#settlements_name {
751            #(#field_names: #field_names,)*
752        })
753    };
754    for index in (0..field_names.len()).rev() {
755        let field = field_names[index];
756        let field_ty = field_types[index];
757        let prior_fields = &field_names[..index];
758        let later_fields = &field_names[index + 1..];
759        let later_types = &field_types[index + 1..];
760        let on_complete = interpretation;
761        interpretation = quote! {
762            match <#field_ty as #behavior::InterpretSends<
763                __BombayInterpreter,
764                __BombayRootEvent,
765                __BombayPath,
766            >>::interpret(self.#field, interpreter).await {
767                #behavior::Interpretation::Complete(#field) => #on_complete,
768                #behavior::Interpretation::Corrupt(#field) => {
769                    #behavior::Interpretation::Corrupt(#settlements_name {
770                        #(#prior_fields: #prior_fields,)*
771                        #field: #field,
772                        #(
773                            #later_fields: <#later_types as #behavior::SendSettlements>::unattempted(
774                                self.#later_fields,
775                            ),
776                        )*
777                    })
778                }
779            }
780        };
781    }
782    let custody_fields = field_names.iter().enumerate().map(|(index, field)| {
783        let prior_fields = &field_names[..index];
784        let later_fields = &field_names[index + 1..];
785        quote! {
786            let #field = match #behavior::SourceSettlementCustody::offer_next_to_source(
787                self.#field,
788                host,
789            ).await {
790                #behavior::SourceCustody::Exhausted(#field) => #field,
791                #behavior::SourceCustody::Retained(#field) => {
792                    terminal_custody = __BombayTerminalCustody::Required;
793                    #field
794                }
795                #behavior::SourceCustody::Admitted(#field) => {
796                    return #behavior::SourceCustody::Admitted(#settlements_name {
797                        #(#prior_fields: #prior_fields,)*
798                        #field: #field,
799                        #(#later_fields: self.#later_fields,)*
800                    });
801                }
802                #behavior::SourceCustody::Closed(#field) => {
803                    return #behavior::SourceCustody::Closed(#settlements_name {
804                        #(#prior_fields: #prior_fields,)*
805                        #field: #field,
806                        #(#later_fields: self.#later_fields,)*
807                    });
808                }
809            };
810        }
811    });
812
813    let items = quote! {
814        #(pub enum #lane_names {})*
815
816        pub struct #name #generics {
817            #(pub #field_names: #field_types,)*
818        }
819
820        #[doc(hidden)]
821        pub struct #settlements_name #settlement_impl_generics #settlement_where_clause {
822            #(
823                pub #field_names: <#field_types as #behavior::SendSettlements>::Settlements,
824            )*
825        }
826
827        impl #classification_impl_generics #behavior::ClassifySettlement
828            for #settlements_name #classification_type_generics #classification_where_clause
829        {
830            fn settlement_status(&self) -> #behavior::SettlementStatus {
831                #combined_status
832            }
833        }
834
835        impl #custody_impl_generics
836            #behavior::SourceSettlementCustody<
837                __BombaySettlementHost,
838                __BombaySettlementEvent,
839            >
840            for #settlements_name #settlement_type_generics #custody_where_clause
841        {
842            fn offer_next_to_source(
843                self,
844                host: &mut __BombaySettlementHost,
845            ) -> impl ::core::future::Future<
846                Output = #behavior::SourceCustody<Self>,
847            > + ::core::marker::Send {
848                async move {
849                    enum __BombayTerminalCustody {
850                        Unrequired,
851                        Required,
852                    }
853                    let mut terminal_custody = __BombayTerminalCustody::Unrequired;
854                    #(#custody_fields)*
855                    let settlements = #settlements_name {
856                        #(#field_names: #field_names,)*
857                    };
858                    match terminal_custody {
859                        __BombayTerminalCustody::Unrequired => {
860                            #behavior::SourceCustody::Exhausted(settlements)
861                        }
862                        __BombayTerminalCustody::Required => {
863                            #behavior::SourceCustody::Retained(settlements)
864                        }
865                    }
866                }
867            }
868        }
869
870        /// Fluent, statically routed send-lane methods for this behavior's
871        /// generated action product.
872        pub trait #actions_name #action_trait_generics: ::core::marker::Sized {
873            #(#action_trait_methods)*
874        }
875
876        impl #actions_impl_generics #actions_name #type_generics
877            for #behavior::Actions<
878                __BombayAddress,
879                __BombayPh,
880                #name #type_generics,
881                __BombayBirth,
882            >
883            #actions_where_clause
884        {
885            #(#action_impl_methods)*
886        }
887
888        impl #effects_impl_generics #behavior::SendEffects for #name #type_generics
889            #effects_where_clause
890        {
891            fn empty() -> Self {
892                Self {
893                    #(#field_names: <#field_types as #behavior::SendEffects>::empty(),)*
894                }
895            }
896
897            fn append(&mut self, other: Self) {
898                #(
899                    <#field_types as #behavior::SendEffects>::append(
900                        &mut self.#field_names,
901                        other.#field_names,
902                    );
903                )*
904            }
905        }
906
907        impl #lawful_impl_generics #behavior::SendsFor<__BombayEvent>
908            for #name #type_generics #lawful_where_clause
909        {}
910
911        impl #settlement_impl_generics #behavior::SendSettlements
912            for #name #type_generics #settlement_where_clause
913        {
914            type Settlements = #settlements_name #settlement_type_generics;
915
916            fn unattempted(self) -> Self::Settlements {
917                #settlements_name {
918                    #(
919                        #field_names: <#field_types as #behavior::SendSettlements>::unattempted(
920                            self.#field_names,
921                        ),
922                    )*
923                }
924            }
925        }
926
927        #(#send_impls)*
928
929        impl #interpret_impl_generics
930            #behavior::InterpretSends<__BombayInterpreter, __BombayRootEvent, __BombayPath>
931            for #name #type_generics #interpret_where_clause
932        {
933            fn interpret(
934                self,
935                interpreter: &mut __BombayInterpreter,
936            ) -> impl ::core::future::Future<
937                Output = #behavior::Interpretation<Self::Settlements>,
938            > + ::core::marker::Send {
939                async move {
940                    #interpretation
941                }
942            }
943        }
944    };
945    (quote!(#name #type_generics), items)
946}
947
948fn generate_births(
949    product: Option<BirthsSpec>,
950    disposition: Option<CreationSettlementDisposition>,
951    actor: &Ident,
952    _addr: &Type,
953    item: &ItemImpl,
954    behavior: &TokenStream2,
955) -> (TokenStream2, TokenStream2) {
956    let product = match product {
957        None => return (quote!(#behavior::NoBirths), quote!()),
958        Some(BirthsSpec::Existing(ty)) => {
959            if disposition.is_none() {
960                return (quote!(#ty), quote!());
961            }
962            let child = quote!(<#ty as #behavior::BirthMode>::Child);
963            let birth = match disposition {
964                Some(CreationSettlementDisposition::ReturnToCreator) => {
965                    quote!(#behavior::Births<#child>)
966                }
967                Some(CreationSettlementDisposition::RetainForRetirement) => {
968                    quote!(#behavior::RetirementBirths<#child>)
969                }
970                None => unreachable!("a no-birth declaration has no settlement disposition"),
971            };
972            return (birth, quote!());
973        }
974        Some(BirthsSpec::Generated(product)) => product,
975    };
976    let name = format_ident!("{}Children", actor);
977    let roles_name = format_ident!("{}Child", actor);
978    let role_names = product
979        .fields
980        .iter()
981        .map(|field| lane_name(&name, &field.name))
982        .collect::<Vec<_>>();
983    let role_values = product
984        .fields
985        .iter()
986        .map(|field| format_ident!("{}", pascal_name(&field.name)))
987        .collect::<Vec<_>>();
988    let declared_child_types = product
989        .fields
990        .iter()
991        .map(|field| &field.ty)
992        .collect::<Vec<_>>();
993    let role_positions = (0..product.fields.len())
994        .map(|index| {
995            (0..(product.fields.len() - index - 1)).fold(
996                quote!(#behavior::ChildHead),
997                |position, _| quote!(#behavior::ChildTail<#position>),
998            )
999        })
1000        .collect::<Vec<_>>();
1001    let product_generics = product_generics(item, &product.fields);
1002    let (_, type_generics, _) = product_generics.split_for_impl();
1003    let generics = &product_generics;
1004    let parent = &item.self_ty;
1005    let (parent_impl_generics, _, parent_where_clause) = item.generics.split_for_impl();
1006    let choice = match declared_child_types.as_slice() {
1007        [child] => quote!(#child),
1008        children => children.iter().fold(
1009            quote!(#behavior::Never),
1010            |tail, child| quote!(#behavior::ChildChoice<#child, #tail>),
1011        ),
1012    };
1013    let birth = match disposition {
1014        Some(CreationSettlementDisposition::ReturnToCreator) => {
1015            quote!(#behavior::Births<#name #type_generics>)
1016        }
1017        Some(CreationSettlementDisposition::RetainForRetirement) => {
1018            quote!(#behavior::RetirementBirths<#name #type_generics>)
1019        }
1020        None => unreachable!("a generated birth product has an explicit settlement disposition"),
1021    };
1022    (
1023        birth,
1024        quote! {
1025            pub type #name #generics = #choice;
1026
1027            #(
1028                #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
1029                pub struct #role_names;
1030
1031                impl #parent_impl_generics #behavior::ChildRole<#parent> for #role_names
1032                    #parent_where_clause
1033                {
1034                    type Child = #declared_child_types;
1035                    type Position = #role_positions;
1036                }
1037
1038                impl #parent_impl_generics #behavior::ChildOccurrence<#parent> for #role_names
1039                    #parent_where_clause
1040                {
1041                    type Resolution = #behavior::DeclaredChildOccurrence;
1042                }
1043            )*
1044
1045            pub struct #roles_name;
1046
1047            #[allow(
1048                non_upper_case_globals,
1049                reason = "generated role values retain the application-authored field spelling"
1050            )]
1051            impl #roles_name {
1052                #(pub const #role_values: #role_names = #role_names;)*
1053            }
1054
1055        },
1056    )
1057}
1058
1059/// Generate the mechanical `Behavior` implementation for a normal inherent
1060/// impl containing `receive(&mut self, from, message)` and, optionally,
1061/// `init(&mut self)`. Omitting `init` selects the behavior algebra's empty
1062/// initialization transition. The original impl and methods are preserved
1063/// unchanged.
1064#[proc_macro_attribute]
1065#[allow(
1066    clippy::too_many_lines,
1067    reason = "validation and the one coherent Behavior expansion remain in one entry point"
1068)]
1069pub fn behavior(args: TokenStream, item: TokenStream) -> TokenStream {
1070    let args = parse_macro_input!(args as BehaviorArgs);
1071    let item = parse_macro_input!(item as ItemImpl);
1072
1073    if item.trait_.is_some() {
1074        return Error::new_spanned(
1075            &item,
1076            "#[behavior] applies to an inherent impl, not a trait impl",
1077        )
1078        .to_compile_error()
1079        .into();
1080    }
1081
1082    let init = item.items.iter().find_map(|item| match item {
1083        ImplItem::Fn(method) if method.sig.ident == "init" => Some(method),
1084        _ => None,
1085    });
1086    let receive = item.items.iter().find_map(|item| match item {
1087        ImplItem::Fn(method) if method.sig.ident == "receive" => Some(method),
1088        _ => None,
1089    });
1090    let creations_settled = item.items.iter().find_map(|item| match item {
1091        ImplItem::Fn(method) if method.sig.ident == "creations_settled" => Some(method),
1092        _ => None,
1093    });
1094    let Some(receive) = receive else {
1095        return Error::new_spanned(
1096            &item.self_ty,
1097            "#[behavior] requires a receive(&mut self, from, message) method",
1098        )
1099        .to_compile_error()
1100        .into();
1101    };
1102    if let Err(error) = init
1103        .map_or(Ok(()), validate_receiver)
1104        .and_then(|()| validate_receiver(receive))
1105    {
1106        return error.to_compile_error().into();
1107    }
1108    if let Some(init) = init
1109        && init.sig.inputs.len() != 1
1110    {
1111        return Error::new_spanned(&init.sig, "init must accept exactly &mut self")
1112            .to_compile_error()
1113            .into();
1114    }
1115    if receive.sig.inputs.len() != 3 {
1116        return Error::new_spanned(
1117            &receive.sig,
1118            "receive must accept exactly &mut self, from, and message",
1119        )
1120        .to_compile_error()
1121        .into();
1122    }
1123    match (args.creation_settlements, creations_settled) {
1124        (Some(CreationSettlementDisposition::ReturnToCreator), Some(method)) => {
1125            if let Err(error) = validate_receiver(method) {
1126                return error.to_compile_error().into();
1127            }
1128            if method.sig.inputs.len() != 2 {
1129                return Error::new_spanned(
1130                    &method.sig,
1131                    "creations_settled must accept exactly &mut self and one CreationsSettled value",
1132                )
1133                .to_compile_error()
1134                .into();
1135            }
1136        }
1137        (Some(CreationSettlementDisposition::ReturnToCreator), None) => {
1138            return Error::new_spanned(
1139                &item.self_ty,
1140                "`creation_settlements = return_to_creator` requires a creations_settled transition",
1141            )
1142            .to_compile_error()
1143            .into();
1144        }
1145        (Some(CreationSettlementDisposition::RetainForRetirement), Some(method)) => {
1146            return Error::new_spanned(
1147                &method.sig,
1148                "retirement-owned creation settlements do not enter a creations_settled transition",
1149            )
1150            .to_compile_error()
1151            .into();
1152        }
1153        (Some(CreationSettlementDisposition::RetainForRetirement), None) | (None, None) => {}
1154        (None, Some(_)) => {}
1155    }
1156
1157    let BehaviorArgs {
1158        addr,
1159        message,
1160        sends,
1161        births,
1162        creation_settlements,
1163        error,
1164    } = args;
1165    let self_ty = &item.self_ty;
1166    let Type::Path(self_path) = self_ty.as_ref() else {
1167        return Error::new_spanned(self_ty, "#[behavior] requires a nominal actor type")
1168            .to_compile_error()
1169            .into();
1170    };
1171    let Some(self_name) = self_path.path.segments.last().map(|segment| &segment.ident) else {
1172        return Error::new_spanned(self_ty, "#[behavior] requires a nominal actor type")
1173            .to_compile_error()
1174            .into();
1175    };
1176    let (impl_generics, _, where_clause) = item.generics.split_for_impl();
1177    let behavior = match behavior_crate() {
1178        Ok(behavior) => behavior,
1179        Err(error) => return error.to_compile_error().into(),
1180    };
1181    let error = error.map_or_else(|| quote!(#behavior::Never), |error| quote!(#error));
1182    let initialize = init.map_or_else(
1183        || quote!(::core::result::Result::Ok(#behavior::Actions::cont())),
1184        |_| quote!(<#self_ty>::init(self)),
1185    );
1186
1187    let (sends_ty, sends_items) = generate_sends(sends, self_name, &item, &behavior);
1188    let (births_ty, births_items) = generate_births(
1189        births,
1190        creation_settlements,
1191        self_name,
1192        &addr,
1193        &item,
1194        &behavior,
1195    );
1196    let (event_ty, transition) = match creation_settlements {
1197        Some(CreationSettlementDisposition::ReturnToCreator) => {
1198            let child = quote!(<#births_ty as #behavior::BirthMode>::Child);
1199            (
1200                quote!(#behavior::CreationEvent<#addr, #child, #message>),
1201                quote! {
1202                    match event {
1203                        #behavior::CreationEvent::Settlements(settlements) => {
1204                            <#self_ty>::creations_settled(self, settlements)
1205                        }
1206                        #behavior::CreationEvent::User(event) => {
1207                            <#self_ty>::receive(self, event.from, event.message)
1208                        }
1209                    }
1210                },
1211            )
1212        }
1213        Some(CreationSettlementDisposition::RetainForRetirement) | None => (
1214            quote!(#behavior::User<#addr, #message>),
1215            quote!(<#self_ty>::receive(self, event.from, event.message)),
1216        ),
1217    };
1218
1219    quote! {
1220        #sends_items
1221        #births_items
1222        #item
1223
1224        impl #impl_generics #behavior::Protocol for #self_ty #where_clause {
1225            type Addr = #addr;
1226            type Msg = #message;
1227        }
1228
1229        impl #impl_generics #behavior::Behavior for #self_ty #where_clause {
1230            type Protocol = Self;
1231            type Event = #event_ty;
1232            type Sends = #sends_ty;
1233            type Ph = #behavior::Never;
1234            type Error = #error;
1235            type Birth = #births_ty;
1236
1237            fn init(
1238                &mut self,
1239                _: #behavior::InitializationTurn,
1240            ) -> #behavior::BehaviorActed<Self> {
1241                #initialize
1242            }
1243
1244            fn transition(
1245                &mut self,
1246                _: #behavior::ActiveTurn,
1247                event: Self::Event,
1248            ) -> #behavior::BehaviorActed<Self> {
1249                #transition
1250            }
1251        }
1252
1253        impl #impl_generics #behavior::BehaviorBase for #self_ty #where_clause {
1254            type Base = Self;
1255
1256            fn base(&self) -> &Self {
1257                self
1258            }
1259        }
1260    }
1261    .into()
1262}
1263
1264/// Derive one pool worker's ordinary actor protocol and behavior from its
1265/// assignment transition.
1266///
1267/// This attribute owns syntax only. The generated send product is the existing
1268/// typed parent request carrying the opaque completion produced by
1269/// `Assignment::complete`; assignment authority and delivery remain owned by
1270/// the Behavior Actors pool protocol and the runtime interpreter.
1271#[proc_macro_attribute]
1272pub fn pool_worker(args: TokenStream, item: TokenStream) -> TokenStream {
1273    let PoolWorkerArgs { addr, result } = parse_macro_input!(args as PoolWorkerArgs);
1274    let mut item = parse_macro_input!(item as ItemImpl);
1275
1276    if item.trait_.is_some() {
1277        return Error::new_spanned(
1278            &item,
1279            "#[pool_worker] applies to an inherent impl, not a trait impl",
1280        )
1281        .to_compile_error()
1282        .into();
1283    }
1284
1285    let self_ty = item.self_ty.clone();
1286    if !matches!(self_ty.as_ref(), Type::Path(path) if path.qself.is_none()) {
1287        return Error::new_spanned(&self_ty, "#[pool_worker] requires a nominal worker type")
1288            .to_compile_error()
1289            .into();
1290    }
1291
1292    let actors = match actors_crate() {
1293        Ok(actors) => actors,
1294        Err(error) => return error.to_compile_error().into(),
1295    };
1296    let behavior = match behavior_crate() {
1297        Ok(behavior) => behavior,
1298        Err(error) => return error.to_compile_error().into(),
1299    };
1300    let assignment = {
1301        let mut transitions = item.items.iter_mut().filter_map(|item| match item {
1302            ImplItem::Fn(method) if method.sig.ident == "transition" => Some(method),
1303            _ => None,
1304        });
1305        let Some(transition) = transitions.next() else {
1306            return Error::new_spanned(
1307                &self_ty,
1308                "#[pool_worker] requires transition(&mut self, Assignment<Job>)",
1309            )
1310            .to_compile_error()
1311            .into();
1312        };
1313        if let Some(duplicate) = transitions.next() {
1314            return Error::new_spanned(
1315                &duplicate.sig,
1316                "#[pool_worker] accepts exactly one transition method",
1317            )
1318            .to_compile_error()
1319            .into();
1320        }
1321        let assignment = match pool_worker_assignment(transition) {
1322            Ok(assignment) => assignment,
1323            Err(error) => return error.to_compile_error().into(),
1324        };
1325        transition.sig.output = parse_quote!(-> #behavior::BehaviorActed<Self>);
1326        assignment
1327    };
1328    let (impl_generics, _, where_clause) = item.generics.split_for_impl();
1329
1330    quote! {
1331        #item
1332
1333        impl #impl_generics #behavior::Protocol for #self_ty #where_clause {
1334            type Addr = #addr;
1335            type Msg = #assignment;
1336        }
1337
1338        impl #impl_generics #behavior::Behavior for #self_ty #where_clause {
1339            type Protocol = Self;
1340            type Event = #behavior::User<#addr, #assignment>;
1341            type Sends = #behavior::InterpreterRequests<
1342                #behavior::ReportToParent<#actors::atomic::Completion<#result>>
1343            >;
1344            type Ph = #behavior::Never;
1345            type Error = #behavior::Never;
1346            type Birth = #behavior::NoBirths;
1347
1348            fn transition(
1349                &mut self,
1350                _: #behavior::ActiveTurn,
1351                event: Self::Event,
1352            ) -> #behavior::BehaviorActed<Self> {
1353                <#self_ty>::transition(self, event.message)
1354            }
1355        }
1356
1357        impl #impl_generics #behavior::BehaviorBase for #self_ty #where_clause {
1358            type Base = Self;
1359
1360            fn base(&self) -> &Self {
1361                self
1362            }
1363        }
1364    }
1365    .into()
1366}
1367
1368#[cfg(test)]
1369mod tests {
1370    use super::*;
1371
1372    fn permutations(values: &mut [&str], at: usize, output: &mut Vec<String>) {
1373        if at == values.len() {
1374            output.push(values.join(", "));
1375            return;
1376        }
1377        for index in at..values.len() {
1378            values.swap(at, index);
1379            permutations(values, at + 1, output);
1380            values.swap(at, index);
1381        }
1382    }
1383
1384    #[test]
1385    fn every_argument_order_parses() {
1386        let mut declarations = [
1387            "addr = u64",
1388            "message = String",
1389            "sends = { first: Vec<u8>, second: Vec<u16> }",
1390            "births = { first: u8, second: u16 }",
1391            "creation_settlements = retain_for_retirement",
1392            "error = String",
1393        ];
1394        let mut cases = Vec::new();
1395        permutations(&mut declarations, 0, &mut cases);
1396        assert_eq!(cases.len(), 720);
1397        for case in cases {
1398            syn::parse_str::<BehaviorArgs>(&case)
1399                .unwrap_or_else(|error| panic!("failed to parse `{case}`: {error}"));
1400        }
1401    }
1402
1403    #[test]
1404    fn capability_omission_and_trailing_comma_parse() {
1405        syn::parse_str::<BehaviorArgs>("message = (), addr = u8,")
1406            .expect("only required protocol declarations are sufficient");
1407        for declaration in ["NoBirths", "behavior::NoBirths"] {
1408            syn::parse_str::<BehaviorArgs>(&format!(
1409                "message = (), addr = u8, births = {declaration}"
1410            ))
1411            .unwrap_or_else(|error| panic!("explicit no-birth declaration did not parse: {error}"));
1412        }
1413    }
1414
1415    #[test]
1416    fn invalid_declaration_states_are_rejected() {
1417        for case in [
1418            "message = ()",
1419            "addr = u8",
1420            "addr = u8, message = (), addr = u16",
1421            "addr = u8, message = (), unknown = u8",
1422            "addr = u8, message = (), sends = {}",
1423            "addr = u8, message = (), births = {}",
1424            "addr = u8, message = (), births = {}, creation_settlements = retain_for_retirement",
1425            "addr = u8, message = (), sends = { same: Vec<u8>, same: Vec<u8> }",
1426            "addr = u8, message = (), births = { same: u8, same: u16 }, creation_settlements = retain_for_retirement",
1427            "addr = u8, message = (), creation_settlements = retain_for_retirement",
1428            "addr = u8, message = (), births = { child: u8 }, creation_settlements = discard",
1429            "addr = u8, message = (), births = { child: u8 }, creation_settlements = retain_for_retirement, creation_settlements = return_to_creator",
1430        ] {
1431            assert!(
1432                syn::parse_str::<BehaviorArgs>(case).is_err(),
1433                "invalid declaration parsed: `{case}`"
1434            );
1435        }
1436    }
1437
1438    #[test]
1439    fn pool_worker_arguments_require_one_address_and_result() {
1440        for valid in [
1441            "addr = u64, result = String",
1442            "result = String, addr = u64,",
1443        ] {
1444            syn::parse_str::<PoolWorkerArgs>(valid)
1445                .unwrap_or_else(|error| panic!("failed to parse `{valid}`: {error}"));
1446        }
1447        for invalid in [
1448            "addr = u64",
1449            "result = String",
1450            "addr = u64, result = String, addr = u32",
1451            "addr = u64, result = String, result = u8",
1452            "addr = u64, result = String, message = u8",
1453        ] {
1454            assert!(
1455                syn::parse_str::<PoolWorkerArgs>(invalid).is_err(),
1456                "invalid pool worker arguments parsed: `{invalid}`"
1457            );
1458        }
1459    }
1460
1461    #[test]
1462    fn pool_worker_transition_requires_the_domain_source_shape() {
1463        let valid: ItemImpl = syn::parse_str(
1464            "impl Worker { fn transition(&mut self, assignment: Assignment<Job>) \
1465             -> WorkerActed<Self> { result() } }",
1466        )
1467        .expect("valid inherent worker declaration parses");
1468        let method = valid
1469            .items
1470            .iter()
1471            .find_map(|item| match item {
1472                ImplItem::Fn(method) => Some(method),
1473                _ => None,
1474            })
1475            .expect("fixture declares one method");
1476        let assignment = pool_worker_assignment(method)
1477            .unwrap_or_else(|error| panic!("valid worker transition rejected: {error}"));
1478        assert_eq!(quote!(#assignment).to_string(), "Assignment < Job >");
1479
1480        for invalid in [
1481            "impl Worker { fn transition(self, assignment: Assignment<Job>) -> WorkerActed<Self> { result() } }",
1482            "impl Worker { async fn transition(&mut self, assignment: Assignment<Job>) -> WorkerActed<Self> { result() } }",
1483            "impl Worker { fn transition(&mut self, job: Job) -> WorkerActed<Self> { result() } }",
1484            "impl Worker { fn transition(&mut self, assignment: Assignment<Job>) -> WorkerActed<Job> { result() } }",
1485            "impl Worker { fn transition(&mut self, assignment: Assignment<Job>) -> Result<(), ()> { result() } }",
1486        ] {
1487            let declaration: ItemImpl = syn::parse_str(invalid)
1488                .unwrap_or_else(|error| panic!("invalid fixture did not parse: {error}"));
1489            let method = declaration
1490                .items
1491                .iter()
1492                .find_map(|item| match item {
1493                    ImplItem::Fn(method) => Some(method),
1494                    _ => None,
1495                })
1496                .expect("fixture declares one method");
1497            assert!(
1498                pool_worker_assignment(method).is_err(),
1499                "invalid pool worker transition was accepted: `{invalid}`"
1500            );
1501        }
1502    }
1503}