1use 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 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 return Ok(quote!(::behavior));
53 }
54 if std::env::var("CARGO_PKG_NAME").as_deref() == Ok("bombay-rs") {
55 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(¶meter.lifetime.ident.to_string()),
515 GenericParam::Type(parameter) => {
516 uses.identifiers.contains(¶meter.ident.to_string())
517 }
518 GenericParam::Const(parameter) => {
519 uses.identifiers.contains(¶meter.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 #[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 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#[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#[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}