Forráskód Böngészése

Started implementing code generation for client handles.

Matthew Carr 1 éve
szülő
commit
cbc5e22ed9

+ 20 - 20
Cargo.lock

@@ -70,7 +70,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.42",
+ "syn 2.0.48",
 ]
 
 [[package]]
@@ -432,7 +432,7 @@ dependencies = [
  "proc-macro2",
  "quote",
  "serde",
- "syn 2.0.42",
+ "syn 2.0.48",
 ]
 
 [[package]]
@@ -1071,7 +1071,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.42",
+ "syn 2.0.48",
 ]
 
 [[package]]
@@ -1923,7 +1923,7 @@ dependencies = [
  "proc-macro2",
  "proc-macro2-diagnostics",
  "quote",
- "syn 2.0.42",
+ "syn 2.0.48",
 ]
 
 [[package]]
@@ -2009,7 +2009,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.42",
+ "syn 2.0.48",
 ]
 
 [[package]]
@@ -2122,9 +2122,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.71"
+version = "1.0.78"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8"
+checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
 dependencies = [
  "unicode-ident",
 ]
@@ -2137,7 +2137,7 @@ checksum = "606c4ba35817e2922a308af55ad51bab3645b59eae5c570d4a6cf07e36bd493b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.42",
+ "syn 2.0.48",
  "version_check",
  "yansi",
 ]
@@ -2211,9 +2211,9 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.33"
+version = "1.0.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
+checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
 dependencies = [
  "proc-macro2",
 ]
@@ -2557,9 +2557,9 @@ dependencies = [
 
 [[package]]
 name = "serde"
-version = "1.0.144"
+version = "1.0.196"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
+checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
 dependencies = [
  "serde_derive",
 ]
@@ -2595,20 +2595,20 @@ dependencies = [
 
 [[package]]
 name = "serde_derive"
-version = "1.0.144"
+version = "1.0.196"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94ed3a816fb1d101812f83e789f888322c34e291f894f19590dc310963e87a00"
+checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 1.0.100",
+ "syn 2.0.48",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.92"
+version = "1.0.113"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7434af0dc1cbd59268aa98b4c22c131c0584d2232f6fb166efb993e2832e896a"
+checksum = "69801b70b1c3dac963ecb03a364ba0ceda9cf60c71cfe475e99864759c8b8a79"
 dependencies = [
  "itoa",
  "ryu",
@@ -2748,9 +2748,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.42"
+version = "2.0.48"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b7d0a2c048d661a1a59fcd7355baa232f7ed34e0ee4df2eef3c1c1c0d3852d8"
+checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3154,7 +3154,7 @@ checksum = "3f67b459f42af2e6e1ee213cb9da4dbd022d3320788c3fb3e1b893093f1e45da"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.42",
+ "syn 2.0.48",
 ]
 
 [[package]]

+ 642 - 183
crates/btproto/src/generation.rs

@@ -1,12 +1,14 @@
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
 
+use btrun::model::End;
 use proc_macro2::{Ident, TokenStream};
 use quote::{format_ident, quote, ToTokens};
 
 use crate::{
     case_convert::CaseConvert,
     model::{
-        ActorKind, ActorModel, MethodModel, MsgInfo, OutputKind, ProtocolModel, TypeParamInfo,
+        ActorKind, ActorModel, MethodModel, MsgInfo, ProtocolModel, StateModel, TypeParamInfo,
+        ValueKind, ValueModel,
     },
 };
 
@@ -30,22 +32,6 @@ impl ProtocolModel {
         let all_replies = msg_lookup.msg_iter().all(|msg| msg.is_reply());
         let enum_name = self.msg_enum_ident();
         let enum_kinds_name = self.msg_enum_kinds_ident();
-        let name_decl_vec: Vec<(Ident, TokenStream)> = variants
-            .iter()
-            .map(|variant| {
-                let variant_str = variant_names_map.get(variant.as_ref()).unwrap();
-                let name_ident =
-                    format_ident!("{}_NAME", variant_str.pascal_to_snake().to_uppercase());
-                let decl = quote! {
-                    static #name_ident: Lazy<Arc<String>> = Lazy::new(|| {
-                        Arc::new(#variant_str.into())
-                    });
-                };
-                (name_ident, decl)
-            })
-            .collect();
-        let name_decls = name_decl_vec.iter().map(|(_, decl)| decl);
-        let name_idents = name_decl_vec.iter().map(|(ident, _)| ident);
         let send_impl = if all_replies {
             quote! {}
         } else {
@@ -55,6 +41,7 @@ impl ProtocolModel {
         };
         let proto_name = &self.def().name_def.name;
         let doc_comment = format!("Message type for the {proto_name} protocol.");
+        let name_method = self.generate_name_method(|| variant_names_map.keys().copied());
         quote! {
             #[doc = #doc_comment]
             #[derive(::serde::Serialize, ::serde::Deserialize)]
@@ -70,14 +57,7 @@ impl ProtocolModel {
             }
 
             impl ::btrun::model::Named for #enum_kinds_name {
-                fn name(&self) -> ::std::sync::Arc<String> {
-                    use ::btrun::model::Lazy;
-                    use ::std::sync::Arc;
-                    #( #name_decls )*
-                    match self {
-                        #( Self::#variants => #name_idents.clone() ),*
-                    }
-                }
+                #name_method
             }
 
             impl Copy for #enum_kinds_name {}
@@ -110,7 +90,7 @@ impl ProtocolModel {
         });
         let mut tokens = TokenStream::new();
         for (trait_ident, methods, is_init_state) in traits {
-            let method_tokens = methods.map(|x| x.generate_tokens());
+            let method_tokens = methods.map(|x| x.generate_trait_def());
             let actor_impl_method = if is_init_state {
                 quote! {
                     #[doc = "The name of the implementation for the actor this state is a part of."]
@@ -153,12 +133,12 @@ impl ProtocolModel {
         match actor.kind() {
             ActorKind::Service => self.generate_service_spawn_function(actor),
             ActorKind::Worker => self.generate_worker_spawn_function(actor),
-            ActorKind::Client => TokenStream::default(),
+            ActorKind::Client => self.generate_client_spawn_function(actor),
         }
     }
 
     fn generate_worker_spawn_function(&self, actor: &ActorModel) -> TokenStream {
-        let function_name = actor.spawn_function_ident();
+        let worker_spawn_function_name = actor.spawn_function_ident();
         let TypeParamInfo {
             type_params,
             constraints,
@@ -166,9 +146,9 @@ impl ProtocolModel {
         let init_state_type_param = actor.init_state().type_param();
         let state_enum_decl = self.generate_state_enum(actor);
         let init_state_var = self.init_state_var();
-        let server_loop = self.generate_loop(actor);
+        let server_loop = self.generate_actor_loop(actor);
         quote! {
-            async fn #function_name<#( #type_params ),*>(
+            async fn #worker_spawn_function_name<#( #type_params ),*>(
                 runtime: &'static ::btrun::Runtime,
                 owner_name: ::btrun::model::ActorName,
                 #init_state_var: #init_state_type_param,
@@ -185,7 +165,7 @@ impl ProtocolModel {
     }
 
     fn generate_service_spawn_function(&self, actor: &ActorModel) -> TokenStream {
-        let function_name = format_ident!("register_{}", actor.def().actor.as_ref());
+        let service_spawn_function_name = format_ident!("register_{}", actor.def().actor.as_ref());
         let TypeParamInfo {
             type_params,
             constraints,
@@ -194,9 +174,9 @@ impl ProtocolModel {
         let state_enum_decl = self.generate_state_enum(actor);
         let msg_enum = self.msg_enum_ident();
         let init_state_var = self.init_state_var();
-        let server_loop = self.generate_loop(actor);
+        let server_loop = self.generate_actor_loop(actor);
         quote! {
-            async fn #function_name<#( #type_params ),*, F>(
+            async fn #service_spawn_function_name<#( #type_params ),*, F>(
                 runtime: &'static ::btrun::Runtime,
                 service_id: ::btrun::model::ServiceId,
                 make_init: F,
@@ -225,13 +205,91 @@ impl ProtocolModel {
         }
     }
 
-    fn generate_loop(&self, actor: &ActorModel) -> TokenStream {
+    fn generate_client_spawn_function(&self, actor: &ActorModel) -> TokenStream {
+        let init_state_model = actor.init_state();
+        let init_state = init_state_model.name();
+        let init_type_param = init_state_model.type_param();
+        let state_enum = self.generate_client_state_enum(actor);
+        let state_enum_ident = actor.state_enum_ident();
+        let handle_struct = self.generate_client_handle(actor);
+        let handle_struct_ident = actor.handle_struct_ident().unwrap();
+        let client_spawn_function_name = actor.spawn_function_ident().unwrap();
+        let TypeParamInfo {
+            type_params,
+            constraints,
+        } = self.type_param_info_for(actor.name());
+        let init_var = self.init_state_var();
+        let use_statements = self.use_statements();
+        let actor_id_param = self.actor_id_param();
+        let runtime_var = self.runtime_param();
+        let state_var = self.state_var();
+        let actor_name_ident = self.actor_name_ident();
+        let msg_enum = self.msg_enum_ident();
+        let msg = self.msg_ident();
+        let from = self.from_ident();
+        let reply = self.reply_ident();
+        let call_transitions = self.generate_call_transitions(actor);
+        let send_transitions = self.generate_send_transitions(actor);
+        let control_transitions = self.generate_control_transitions(actor);
+        let end_ident = self.end_ident();
+        quote! {
+            #state_enum
+            #handle_struct
+
+            async fn #client_spawn_function_name<#( #type_params ),* >(
+                #init_var: #init_type_param,
+                runtime: &'static ::btrun::Runtime,
+            ) -> #handle_struct_ident<#init_type_param, #init_type_param>
+            where
+                #( #constraints ),*
+            {
+                #use_statements
+                let shared_state = Arc::new(Mutex::new(Some(#state_enum_ident::#init_state(#init_var))));
+                let name = {
+                    let shared_state = shared_state.clone();
+                    runtime.spawn(None, move |mut mailbox: Mailbox<#msg_enum>, #actor_id_param, #runtime_var| async move {
+                        let #actor_name_ident = runtime.actor_name(#actor_id_param);
+                        while let Some(envelope) = mailbox.recv().await {
+                            let mut guard = shared_state.lock().await;
+                            let #state_var = guard.take()
+                                .unwrap_or_else(|| {
+                                    panic!(
+                                        "Logic error. The shared state for client {} was not returned.",
+                                        #init_type_param::actor_impl()
+                                    )
+                                });
+                            let new_state = match envelope {
+                                Envelope::Call { from: #from, msg: #msg, reply: #reply, .. } => {
+                                    #call_transitions
+                                }
+                                Envelope::Send { from: #from, msg: #msg, .. } => {
+                                    #send_transitions
+                                }
+                                Envelope::Control(#msg) => {
+                                    #control_transitions
+                                }
+                            };
+                            *guard = Some(new_state);
+                            if let Some(state) = &*guard {
+                                if let #state_enum_ident::#end_ident(_) = state {
+                                    break;
+                                }
+                            }
+                        }
+                        Ok(#actor_id_param)
+                    }).await.unwrap()
+                };
+                #handle_struct_ident::new(runtime, shared_state, name)
+            }
+        }
+    }
+
+    fn generate_actor_loop(&self, actor: &ActorModel) -> TokenStream {
         let init_state = actor.init_state().name();
         let state_enum_ident = actor.state_enum_ident();
         let msg_enum = self.msg_enum_ident();
         let init_state_var = self.init_state_var();
         let end_ident = self.end_ident();
-        let mailbox = self.mailbox_param();
         let actor_id = self.actor_id_param();
         let runtime = self.runtime_param();
         let from = self.from_ident();
@@ -241,29 +299,22 @@ impl ProtocolModel {
         let call_transitions = self.generate_call_transitions(actor);
         let send_transitions = self.generate_send_transitions(actor);
         let control_transitions = self.generate_control_transitions(actor);
+        let use_statements = self.use_statements();
         quote! {
             move |
-                mut #mailbox: ::btrun::Mailbox<#msg_enum>,
+                mut mailbox: ::btrun::Mailbox<#msg_enum>,
                 #actor_id: ::btrun::model::ActorId,
                 #runtime: &'static ::btrun::Runtime
             | async move {
-                use ::btlib::bterr;
-                use ::btrun::{
-                    log,
-                    model::{
-                        Envelope, ControlMsg, Named, TransResult, ActorError, ActorErrorPayload,
-                        TransKind,
-                    }
-                };
+                #use_statements
                 let #actor_name = #runtime . actor_name(#actor_id);
                 let mut state = #state_enum_ident :: #init_state (#init_state_var);
-                while let Some(envelope) = #mailbox.recv().await {
-                    let new_state = match envelope {
+                while let Some(envelope) = mailbox.recv().await {
+                    state = match envelope {
                         Envelope::Call { #msg, #from, mut #reply, .. } => #call_transitions
                         Envelope::Send { #msg, #from, .. } => #send_transitions
                         Envelope::Control(#msg) => #control_transitions
                     };
-                    state = new_state;
                     if let #state_enum_ident::#end_ident(_) = &state {
                         break;
                     }
@@ -288,9 +339,6 @@ impl ProtocolModel {
     ) -> TokenStream {
         let state_enum_ident = actor.state_enum_ident();
         let msg_enum_ident = self.msg_enum_ident();
-        let init_state_type_param = actor.init_state().type_param();
-        let msg_enum_kinds = self.msg_enum_kinds_ident();
-        let actor_id = self.actor_id_param();
         let transitions = actor.states().values().flat_map(|state| {
             state.methods().values().filter(|method| {
                 if let Some(in_msg) = method.def().in_msg() {
@@ -301,128 +349,199 @@ impl ProtocolModel {
                 }
             })
             .map(|method| {
-                let state_type_param = state.type_param();
-                let mut output_iter = method.outputs().iter();
-                let next_state = output_iter.next()
-                    .unwrap_or_else(|| panic!("There are no outputs for method {} in state {}.", method.name(), state.name()));
-                let next_state_name = if let OutputKind::State { def, .. } = next_state.kind() {
-                    def.state_trait.as_ref()
-                } else {
-                    panic!("First output of {} method was not a state.", method.name());
-                };
-                let next_state_var = next_state.var_name();
-                let out_states = output_iter
-                    .flat_map(|output| {
-                        if let OutputKind::State { def, .. } = output.kind() {
-                            Some((output.var_name(), def))
-                        } else {
-                            None
-                        }
-                    })
-                    .map(|(var_name, def)| {
-                        let spawning_actor = self.actor_lookup().actor_with_state(&def.state_trait);
-                        let spawning_model = self.actors().get(spawning_actor)
-                            .unwrap_or_else(|| panic!("There was no actor named {spawning_actor}."));
-                        let spawn_function = spawning_model.spawn_function_ident()
-                            .unwrap_or_else(|| panic!("Actor {spawning_actor} of kind {:?} has no spawn function.", spawning_model.kind()));
-                        let from = self.from_ident();
-                        let runtime = self.runtime_param();
-                        let method_name = method.name();
-                        quote! {
-                            if let Err(err) = #spawn_function(#runtime, #from, #var_name).await {
-                                log::error!(
-                                    "Failed to spawn {} actor after the {} method: {err}",
-                                    stringify!(#spawning_actor),
-                                    stringify!(#method_name)
-                                )
-                            }
-                        }
-                    });
-                let out_msgs = method.outputs().iter()
-                    .flat_map(|output| {
-                        if let OutputKind::Msg { def, .. } = output.kind() {
-                            Some((output.var_name(), def))
-                        } else {
-                            None
-                        }
-                    })
-                    .map(|(var_name, dest)| {
-                        if dest.msg.is_reply() {
-                            let reply = self.reply_ident();
-                            let msg_type = &dest.msg.msg_type;
-                            let reply_variant = self.msg_lookup().lookup(&dest.msg).msg_name();
-                            let error_msg = format!("Failed to send {} reply.", msg_type);
-                            quote! {
-                                if let Some(mut reply) = #reply.take() {
-                                    if let Err(_) = reply.send(#msg_enum_ident :: #reply_variant (#var_name)) {
-                                        return Err(ActorError::new(
-                                            bterr!(#error_msg),
-                                            ActorErrorPayload {
-                                                actor_id: #actor_id,
-                                                actor_impl: #init_state_type_param :: actor_impl(),
-                                                state: #state_type_param :: state_name(),
-                                                message: #msg_enum_kinds :: #msg_type .name(),
-                                                kind: TransKind::Receive,
-                                            }
-                                        ));
-                                    }
-                                } else {
-                                    log::error!(
-                                        "Reply to {} message has already been sent.",
-                                        #msg_enum_kinds :: #msg_type .name()
-                                    );
-                                }
-                            }
-                        } else {
-                            todo!("Send message to an owned state or to a service.");
-                        }
-                    });
-                let method_name = method.name();
                 let state_name = state.name();
-                let msg_name = method.msg_received_input().unwrap().msg_name();
-                let out_vars = method.outputs().iter().map(|output| output.var_name());
+                let msg_name = method.msg_received_input()
+                    .unwrap_or_else(|| panic!("Method '{}' does not handle any messages.", method.name()))
+                    .msg_type
+                    .as_ref();
+                let args = std::iter::once(self.msg_ident());
+                let method_call = self.generate_method_call(actor, state, method, args);
                 quote! {
                     (#state_enum_ident :: #state_name(state), #msg_enum_ident :: #msg_name(msg)) => {
-                        match state.#method_name(msg).await {
-                            TransResult::Ok(( #( #out_vars ),* )) => {
-                                #( #out_states )*
-                                #( #out_msgs )*
-                                #state_enum_ident :: #next_state_name (#next_state_var)
-                            }
-                            TransResult::Abort { from, err, .. } => {
-                                log::warn!(
-                                    "Aborted transition from the {} state while handling the {} message: {}",
-                                    stringify!(#state_name),
-                                    stringify!(#msg_name),
-                                    err
-                                );
-                                #state_enum_ident :: #state_name(from)
-                            }
-                            TransResult::Fatal { err, .. } => {
+                        #method_call
+                    },
+                }
+            })
+        });
+        quote! {
+            match (state, msg) {
+                #( #transitions )*
+                (state, msg) => {
+                    log::error!(
+                        "Unexpected message {} in state {}.", msg.name(), state.name()
+                    );
+                    state
+                }
+            }
+        }
+    }
+
+    fn generate_method_call<TTokens: ToTokens>(
+        &self,
+        actor: &ActorModel,
+        state: &StateModel,
+        method: &MethodModel,
+        args: impl Iterator<Item = TTokens>,
+    ) -> TokenStream {
+        let msg_enum_ident = self.msg_enum_ident();
+        let msg_enum_kinds = self.msg_enum_kinds_ident();
+        let state_type_param = state.type_param();
+        let actor_id = self.actor_id_param();
+        let init_state_type_param = actor.init_state().type_param();
+        let state_enum_ident = actor.state_enum_ident();
+
+        let mut output_iter = method.output_values().iter();
+        let next_state = output_iter.next().unwrap_or_else(|| {
+            panic!(
+                "There are no outputs for method {} in state {}.",
+                method.name(),
+                state.name()
+            )
+        });
+        let next_state_name = if let ValueKind::State { def, .. } = next_state.kind() {
+            def.state_trait.as_ref()
+        } else {
+            panic!("First output of {} method was not a state.", method.name());
+        };
+        let next_state_var = next_state.var_name();
+        let out_states = output_iter
+            .flat_map(|output| {
+                if let ValueKind::State { def, .. } = output.kind() {
+                    Some((output.var_name(), def))
+                } else {
+                    None
+                }
+            })
+            .map(|(var_name, def)| {
+                let spawning_actor = self.actor_lookup().actor_with_state(&def.state_trait);
+                let spawning_model = self
+                    .actors()
+                    .get(spawning_actor)
+                    .unwrap_or_else(|| panic!("There was no actor named {spawning_actor}."));
+                let spawn_function = spawning_model.spawn_function_ident().unwrap_or_else(|| {
+                    panic!(
+                        "Actor {spawning_actor} of kind {:?} has no spawn function.",
+                        spawning_model.kind()
+                    )
+                });
+                let from = self.from_ident();
+                let runtime = self.runtime_param();
+                let method_name = method.name();
+                quote! {
+                    if let Err(err) = #spawn_function(#runtime, #from, #var_name).await {
+                        log::error!(
+                            "Failed to spawn {} actor after the {} method: {err}",
+                            stringify!(#spawning_actor),
+                            stringify!(#method_name)
+                        )
+                    }
+                }
+            });
+        let out_msgs = method.output_values().iter()
+            .flat_map(|output| {
+                if let ValueKind::Msg { def, .. } = output.kind() {
+                    Some((output.var_name(), def))
+                } else {
+                    None
+                }
+            })
+            .map(|(var_name, msg)| {
+                if msg.is_reply() {
+                    let reply = self.reply_ident();
+                    let msg_type = &msg.msg_type;
+                    let reply_variant = self.msg_lookup().lookup(msg).msg_name();
+                    let error_msg = format!("Failed to send '{}'.", msg_type);
+                    quote! {
+                        if let Some(mut reply) = #reply.take() {
+                            if let Err(_) = reply.send(#msg_enum_ident :: #reply_variant (#var_name)) {
                                 return Err(ActorError::new(
-                                    err,
+                                    bterr!(#error_msg),
                                     ActorErrorPayload {
                                         actor_id: #actor_id,
                                         actor_impl: #init_state_type_param :: actor_impl(),
                                         state: #state_type_param :: state_name(),
-                                        message: #msg_enum_kinds :: #msg_name . name(),
-                                        kind: TransKind::Receive
+                                        message: #msg_enum_kinds :: #msg_type .name(),
+                                        kind: TransKind::Receive,
                                     }
                                 ));
                             }
+                        } else {
+                            log::error!(
+                                "Reply to '{}' has already been sent.",
+                                #msg_enum_kinds :: #msg_type .name()
+                            );
                         }
-                    },
+                    }
+                } else {
+                    quote! {}
+                    //match &dest.state {
+                    //    DestinationState::Service(state) => {
+                    //        let msg = &dest.msg;
+                    //        let msg_info = self.msg_lookup().lookup(msg);
+                    //        let runtime = self.runtime_param();
+                    //        if msg_info.is_call() {
+                    //            quote! { todo!("Call a service.") }
+                    //        } else {
+                    //            quote! { todo!("Send to a service.") }
+                    //        }
+                    //    }
+                    //    DestinationState::Individual(state)
+                    //        => quote! { todo!("Send a message to an owned or owner state.") },
+                    //}
+                }
+            });
+        let method_name = method.name();
+        let state_name = state.name();
+        let out_vars = method.output_vars();
+        let (trans_kind, msg_name) = if let Some(input) = method.msg_received_input() {
+            let trans_kind = quote! { TransKind::Receive };
+            let msg_name = input.msg_type.as_ref();
+            (trans_kind, msg_name)
+        } else {
+            let trans_kind = quote! { TransKind::Send };
+            let msg_name = method.output_values().iter().flat_map(|output| {
+                if let ValueKind::Msg { def, .. } = output.kind() {
+                    Some(def.msg_type.as_ref())
+                } else {
+                    None
                 }
             })
-        });
+            .next()
+            .unwrap_or_else(|| {
+                panic!(
+                    "Method '{}' does not receive or send any messages. It should not have passed validation.",
+                    method_name
+                )
+            });
+            (trans_kind, msg_name)
+        };
         quote! {
-            match (state, msg) {
-                #( #transitions )*
-                (state, msg) => {
-                    log::error!(
-                        "Unexpected message {} in state {}.", msg.name(), state.name()
+            match state.#method_name(#( #args ),*).await {
+                TransResult::Ok(( #( #out_vars ),* )) => {
+                    #( #out_states )*
+                    #( #out_msgs )*
+                    #state_enum_ident :: #next_state_name (#next_state_var)
+                }
+                TransResult::Abort { from, err, .. } => {
+                    log::warn!(
+                        "Method {} for actor {} aborted: {}",
+                        stringify!(#method_name),
+                        #init_state_type_param :: actor_impl(),
+                        err
                     );
-                    state
+                    #state_enum_ident :: #state_name(from)
+                }
+                TransResult::Fatal { err, .. } => {
+                    return Err(ActorError::new(
+                        err,
+                        ActorErrorPayload {
+                            actor_id: #actor_id,
+                            actor_impl: #init_state_type_param :: actor_impl(),
+                            state: #state_type_param :: state_name(),
+                            message: #msg_enum_kinds :: #msg_name . name(),
+                            kind: #trans_kind,
+                        }
+                    ));
                 }
             }
         }
@@ -460,47 +579,387 @@ impl ProtocolModel {
                 quote! { #end_ident(::btrun::model::#end_ident) },
             ));
         let type_params = pairs.iter().flat_map(|(_, type_param)| type_param);
-        let name_pairs: Vec<_> = pairs
-            .iter()
-            .map(|(trait_type, _)| {
-                let trait_type_string = trait_type.to_string();
-                let name_ident = format_ident!("{}_NAME", trait_type_string.pascal_to_snake().to_uppercase());
-                let name_decl = quote! {
-                    static #name_ident: ::btrun::model::Lazy<::std::sync::Arc<String>> =
-                        ::btrun::model::Lazy::new(|| ::std::sync::Arc::new(#trait_type_string.into()));
-                };
-                let match_branch = quote! {
-                    Self::#trait_type(_) => #name_ident.clone(),
-                };
-                (name_decl, match_branch)
-            })
-            .collect();
-        let name_decls = name_pairs.iter().map(|(name_decl, _)| name_decl);
-        let name_match_branches = name_pairs.iter().map(|(_, match_branch)| match_branch);
+        let name_method = self.generate_name_method(|| self.state_idents(actor));
         quote! {
             enum #enum_ident #decl_type_params {
                 #( #variants ),*
             }
 
             impl #decl_type_params ::btrun::model::Named for #enum_ident<#( #type_params ),*> {
-                fn name(&self) -> ::std::sync::Arc<String> {
-                    #( #name_decls )*
-                    match self {
-                        #( #name_match_branches )*
+                #name_method
+            }
+        }
+    }
+
+    fn generate_client_state_enum(&self, actor: &ActorModel) -> TokenStream {
+        let init_state = actor.init_state();
+        let init_type_param = self.init_type_param();
+        let paths = {
+            let mut paths = HashMap::<&Ident, TokenStream>::new();
+            let mut visited = HashSet::<&Ident>::new();
+            let mut path = Vec::<&Ident>::new();
+            path.push(init_type_param);
+            self.make_assoc_type_paths(&mut paths, &mut visited, &mut path, init_state);
+            paths
+        };
+        let end_ident = self.end_ident();
+        let variant_decls = actor
+            .states()
+            .values()
+            .map(|state| {
+                let variant = state.name();
+                let path = paths.get(state.name()).unwrap_or_else(|| {
+                    panic!(
+                        "Failed to build a path for state {variant} in actor {}.",
+                        actor.name()
+                    )
+                });
+                quote! { #variant(#path), }
+            })
+            .chain(std::iter::once(
+                quote! { #end_ident(::btrun::model::#end_ident) },
+            ));
+        let enum_name = actor.state_enum_ident();
+        let init_state_name = init_state.name();
+        let name_method = self.generate_name_method(|| self.state_idents(actor));
+        quote! {
+            enum #enum_name<#init_type_param: #init_state_name> {
+                #( #variant_decls )*
+            }
+
+            impl<#init_type_param: #init_state_name> ::btrun::model::Named for #enum_name<#init_type_param> {
+                #name_method
+            }
+        }
+    }
+
+    fn generate_name_method<'a, I, F>(&'a self, get_names: F) -> TokenStream
+    where
+        I: Iterator<Item = &'a Ident>,
+        F: Fn() -> I,
+    {
+        let var_names: Vec<_> = get_names()
+            .map(|name| format_ident!("{}_NAME", name.pascal_to_snake().to_uppercase()))
+            .collect();
+        let decls = get_names().zip(var_names.iter()).map(|(name, var_name)| {
+            quote! {
+                static #var_name: Lazy<Arc<String>> = Lazy::new(|| {
+                    Arc::new(stringify!(#name).into())
+                });
+            }
+        });
+        let names = get_names();
+        quote! {
+            fn name(&self) -> ::std::sync::Arc<String> {
+                use ::std::sync::Arc;
+                use ::btrun::model::Lazy;
+                #( #decls )*
+                match self {
+                    #( Self::#names {..} => #var_names.clone() ),*
+                }
+            }
+        }
+    }
+
+    fn make_assoc_type_paths<'b, 'a: 'b>(
+        &'a self,
+        paths: &'b mut HashMap<&'a Ident, TokenStream>,
+        visited: &'b mut HashSet<&'a Ident>,
+        path: &'b mut Vec<&'a Ident>,
+        current: &'a StateModel,
+    ) {
+        visited.insert(current.name());
+        paths.insert(current.name(), quote! { #( #path )::* });
+        for output in self.next_states(current) {
+            let state_trait = output.kind().state_trait().unwrap().as_ref();
+            if visited.contains(state_trait) {
+                continue;
+            }
+            let next = self.get_state(state_trait);
+            path.push(output.assoc_type().unwrap());
+            self.make_assoc_type_paths(paths, visited, path, next);
+            path.pop();
+        }
+    }
+
+    fn generate_client_handle(&self, actor: &ActorModel) -> TokenStream {
+        let struct_ident = actor.handle_struct_ident().unwrap();
+        let state_enum_ident = actor.state_enum_ident();
+        let init_type_param = actor.init_state().type_param();
+        let init_name = actor.init_state().name();
+        let current_type_param = self.state_type_param();
+        let new_type_param = self.new_state_type_param();
+        let new_state_method = self.new_state_method();
+        let state_field = self.state_field();
+        let transitions = actor.states().values().flat_map(|current_state| {
+            current_state
+                .methods()
+                .values()
+                .filter(|method| method.msg_received_input().is_none())
+                .map(|method| self.generate_client_method(actor, current_state, method))
+        });
+        quote! {
+            struct #struct_ident<#init_type_param: #init_name, #current_type_param> {
+                runtime: &'static ::btrun::Runtime,
+                #state_field: ::std::sync::Arc<::btrun::model::Mutex<Option<#state_enum_ident<#init_type_param>>>>,
+                actor_name: ::btrun::model::ActorName,
+                type_state: ::std::marker::PhantomData<#current_type_param>,
+            }
+
+            impl<#init_type_param: #init_name> #struct_ident<#init_type_param, #init_type_param> {
+                fn new(
+                    runtime: &'static ::btrun::Runtime,
+                    #state_field: ::std::sync::Arc<::btrun::model::Mutex<Option<#state_enum_ident<#init_type_param>>>>,
+                    actor_name: ::btrun::model::ActorName,
+                ) -> Self {
+                    Self {
+                        runtime,
+                        #state_field,
+                        actor_name,
+                        type_state: ::std::marker::PhantomData,
+                    }
+                }
+            }
+
+            impl<#init_type_param: #init_name, #current_type_param> #struct_ident<#init_type_param, #current_type_param> {
+                fn #new_state_method<#new_type_param>(self) -> #struct_ident<#init_type_param, #new_type_param> {
+                    #struct_ident {
+                        runtime: self.runtime,
+                        #state_field: self.#state_field,
+                        actor_name: self.actor_name,
+                        type_state: ::std::marker::PhantomData,
                     }
                 }
             }
+
+            #( #transitions )*
+        }
+    }
+
+    fn generate_client_method(
+        &self,
+        actor: &ActorModel,
+        current_state: &StateModel,
+        method: &MethodModel,
+    ) -> TokenStream {
+        let init_state = actor.init_state();
+        let init_name = init_state.name();
+        let init_type_param = init_state.type_param();
+        let new_type_param = self.new_state_type_param();
+
+        let handle_method_name = method.handle_name().unwrap_or_else(|| {
+            panic!(
+                "Method '{}' in client '{}' had no handle method name.",
+                method.name(),
+                actor.name()
+            )
+        });
+        let current_name = current_state.name();
+        let current_type_param = current_state.type_param();
+        let new_state_out = method.next_state();
+        let new_name = new_state_out.kind().state_trait().unwrap().as_ref();
+        let new_type_param = if new_name == End::ident() {
+            self.end_ident()
+        } else {
+            new_type_param
+        };
+
+        fn current_constraint(
+            current_type_param: &Ident,
+            current_name: &Ident,
+            new_type_param: &Ident,
+            new_state_out: &ValueModel,
+        ) -> TokenStream {
+            new_state_out
+                .assoc_type()
+                .map(|new_assoc_type| {
+                    quote! {
+                        #current_type_param: #current_name<#new_assoc_type = #new_type_param>,
+                    }
+                })
+                .unwrap_or_else(|| {
+                    quote! {
+                        #current_type_param: #current_name,
+                    }
+                })
+        }
+
+        fn new_constraint(new_type_param: &Ident, new_name: &Ident) -> Option<TokenStream> {
+            if new_name == End::ident() {
+                None
+            } else {
+                Some(quote! { #new_type_param: #new_name, })
+            }
+        }
+
+        // We start by assuming that init, current, and new are all different.
+        // This means including type constraints for all three.
+        let type_constraints = {
+            let current_constraint = current_constraint(
+                current_type_param,
+                current_name,
+                new_type_param,
+                new_state_out,
+            );
+            let new_constraint = new_constraint(new_type_param, new_name);
+            quote! {
+                #init_type_param: #init_name,
+                #current_constraint
+                #new_constraint
+            }
+        };
+        #[allow(clippy::collapsible_else_if)]
+        let (type_constraints, current_type_param, new_type_param) = if init_name == current_name {
+            let current_type_param = init_type_param;
+            if current_name == new_name {
+                // All three are the same, so we mut omit the init and new constraints.
+                let new_type_param = init_type_param;
+                let type_constraints = current_constraint(
+                    current_type_param,
+                    current_name,
+                    new_type_param,
+                    new_state_out,
+                );
+                (type_constraints, current_type_param, new_type_param)
+            } else {
+                // init and current are the same, so we must omit the init constraint.
+                let current_constraint = current_constraint(
+                    current_type_param,
+                    current_name,
+                    new_type_param,
+                    new_state_out,
+                );
+                let new_constraint = new_constraint(new_type_param, new_name);
+                let type_constraints = quote! {
+                    #current_constraint
+                    #new_constraint
+                };
+                (type_constraints, current_type_param, new_type_param)
+            }
+        } else {
+            if current_name == new_name {
+                // current and new are the same, so we must omit the new constraint.
+                let new_type_param = current_type_param;
+                let current_constraint = current_constraint(
+                    current_type_param,
+                    current_name,
+                    new_type_param,
+                    new_state_out,
+                );
+                let type_constraints = quote! {
+                    #init_type_param: #init_name,
+                    #current_constraint
+                };
+                (type_constraints, current_type_param, new_type_param)
+            } else {
+                // All three are different, so we must not omit any constraints.
+                (type_constraints, current_type_param, new_type_param)
+            }
+        };
+        // If the new type is End then it must be fully qualified.
+        let end_ident = self.end_ident();
+        let new_type_param = if new_type_param == end_ident {
+            quote! { ::btrun::model::#end_ident}
+        } else {
+            quote! { #new_type_param }
+        };
+
+        let params = method.inputs().iter().map(|input| input.as_handle_param());
+        let first_msg_type = {
+            let first_input = method
+                .inputs()
+                .get(0)
+                .unwrap_or_else(|| panic!("Method '{}' had no inputs.", method.name()));
+            if let ValueKind::Dest { msg_type, .. } = first_input.kind() {
+                msg_type
+            } else {
+                panic!(
+                    "First input to method '{}' was not a destination.",
+                    method.name()
+                )
+            }
+        };
+        let msg_enum_kinds_ident = self.msg_enum_kinds_ident();
+        let actor_name_ident = self.actor_name_ident();
+        let actor_id_param = self.actor_id_param();
+        let struct_ident = actor.handle_struct_ident().unwrap_or_else(|| {
+            panic!(
+                "Attempted to generate client method for non-client actor '{}'.",
+                actor.name()
+            )
+        });
+        let use_statements = self.use_statements();
+        let state_field = self.state_field();
+        let state_enum_ident = actor.state_enum_ident();
+        let new_state_method = self.new_state_method();
+        let runtime_param = self.runtime_param();
+        let from_ident = self.from_ident();
+        let actor_name = self.actor_name_ident();
+        quote! {
+            #[allow(unreachable_code)]
+            impl<#type_constraints> #struct_ident<#init_type_param, #current_type_param> {
+                async fn #handle_method_name(
+                    self,
+                    to: ::btrun::model::ServiceAddr,
+                    #( #params ),*
+                ) -> ::std::result::Result<#struct_ident<#init_type_param, #new_type_param>, ::btrun::model::ActorError> {
+                    #use_statements
+                    let #actor_id_param = self.#actor_name_ident.actor_id();
+                    let #runtime_param = self.#runtime_param;
+                    let #from_ident = &self.#actor_name;
+                    {
+                        let mut guard = self.#state_field.lock().await;
+                        let state = guard
+                            .take()
+                            .ok_or_else(|| {
+                                ActorError::new(
+                                    bterr!("Client shared state was not returned."),
+                                    ActorErrorPayload {
+                                        actor_id: #actor_id_param,
+                                        actor_impl: #init_type_param :: actor_impl(),
+                                        state: #current_type_param :: state_name(),
+                                        message: #msg_enum_kinds_ident::#first_msg_type.name(),
+                                        kind: TransKind::Send,
+                                    }
+                                )
+                            })?;
+                        let new_state = match state {
+                            #state_enum_ident::#current_name(state) => {
+                                todo!()
+                            },
+                            state => {
+                                *guard = Some(state);
+                                return Err(ActorError::new(
+                                    bterr!("Client is in an unexpected state."),
+                                    ActorErrorPayload {
+                                        actor_id: #actor_id_param,
+                                        actor_impl: #init_type_param :: actor_impl(),
+                                        state: #current_type_param :: state_name(),
+                                        message: #msg_enum_kinds_ident::#first_msg_type.name(),
+                                        kind: TransKind::Send,
+                                    }
+                                ))
+                            }
+                        };
+                        *guard = Some(new_state);
+                    }
+                    Ok(self.#new_state_method())
+                }
+            }
         }
     }
 }
 
 impl MethodModel {
-    /// Generates the tokens for the code which implements this transition.
-    fn generate_tokens(&self) -> TokenStream {
+    /// Returns the code which defines this method in its state trait.
+    fn generate_trait_def(&self) -> TokenStream {
         let method_ident = self.name().as_ref();
-        let msg_args = self.inputs().iter();
-        let output_decls = self.outputs().iter().flat_map(|output| output.decl());
-        let output_types = self.outputs().iter().flat_map(|output| output.type_name());
+        let msg_args = self.inputs().iter().map(|input| input.in_method_decl());
+        let output_decls = self.output_values().iter().flat_map(|output| output.decl());
+        let output_types = self
+            .output_values()
+            .iter()
+            .flat_map(|output| output.type_name());
         let future_name = self.future();
         quote! {
             #( #output_decls )*

+ 325 - 176
crates/btproto/src/model.rs

@@ -6,7 +6,7 @@ use std::{
 
 use btrun::model::End;
 use proc_macro2::{Ident, Span, TokenStream};
-use quote::{format_ident, quote, ToTokens};
+use quote::{format_ident, quote, quote_spanned};
 
 use crate::{
     case_convert::CaseConvert,
@@ -19,29 +19,24 @@ pub(crate) struct ProtocolModel {
     def: Protocol,
     msg_lookup: MsgLookup,
     actor_lookup: ActorLookup,
+    use_statements: TokenStream,
     actors: HashMap<Rc<Ident>, ActorModel>,
-    /// The name of the message enum used by this protocol.
     msg_enum_name: Ident,
-    /// The name of the message kinds enum used by this protocol.
     msg_enum_kinds_name: Ident,
-    /// The [Ident] containing [End::ident].
     end_ident: Ident,
-    /// The name of the [btrun::Mailbox] parameter in an actor closure.
-    mailbox_param: Ident,
-    /// The name of the [btrun::ActorId] parameter in an actor closure.
+    end_struct: TokenStream,
     actor_id_param: Ident,
-    /// The name of the `&'static `[Runtime] parameter in an actor closure.
     runtime_param: Ident,
-    /// The name of the variable used to hold the `msg` field from a [btrun::Envelope].
     msg_ident: Ident,
-    /// The name of the variable used to hold the `reply` field from a [btrun::Envelope].
     reply_ident: Ident,
-    /// The name of the variable used to hold the `from` field of a [btrun::Envelope].
     from_ident: Ident,
-    /// The name of the local variable in an actor closure used to hold the actor's name.
     actor_name_ident: Ident,
-    /// The identifier for the variable holding the initial state of an actor.
     init_state_var: Ident,
+    init_state_type_param: Ident,
+    state_type_param: Ident,
+    new_state_type_param: Ident,
+    new_state_method: Ident,
+    state_var: Ident,
 }
 
 impl ProtocolModel {
@@ -111,15 +106,17 @@ impl ProtocolModel {
             actors.insert(actor_name.clone(), actor);
         }
 
+        let end_ident = format_ident!("{}", End::ident());
         Ok(Self {
             msg_enum_name: Self::make_msg_enum_name(&def),
             msg_enum_kinds_name: Self::make_msg_enum_kinds_name(&def),
+            use_statements: Self::make_use_statements(),
             def,
             msg_lookup,
             actor_lookup,
             actors,
-            end_ident: format_ident!("{}", End::ident()),
-            mailbox_param: format_ident!("mailbox"),
+            end_struct: quote! { ::btrun::model::#end_ident },
+            end_ident,
             actor_id_param: format_ident!("actor_id"),
             runtime_param: format_ident!("runtime"),
             msg_ident: format_ident!("msg"),
@@ -127,6 +124,11 @@ impl ProtocolModel {
             from_ident: format_ident!("from"),
             actor_name_ident: format_ident!("actor_name"),
             init_state_var: format_ident!("init"),
+            init_state_type_param: format_ident!("Init"),
+            state_type_param: format_ident!("State"),
+            new_state_type_param: format_ident!("NewState"),
+            new_state_method: format_ident!("new_state"),
+            state_var: format_ident!("state"),
         })
     }
 
@@ -138,6 +140,20 @@ impl ProtocolModel {
         format_ident!("{}MsgKinds", def.name_def.name)
     }
 
+    fn make_use_statements() -> TokenStream {
+        quote! {
+            use ::btlib::bterr;
+            use ::btrun::{
+                log, Mailbox,
+                model::{
+                    Envelope, ControlMsg, Named, TransResult, ActorError, ActorErrorPayload,
+                    TransKind, Mutex,
+                }
+            };
+            use ::std::sync::Arc;
+        }
+    }
+
     pub(crate) fn def(&self) -> &Protocol {
         &self.def
     }
@@ -169,63 +185,126 @@ impl ProtocolModel {
     }
 
     #[cfg(test)]
-    pub(crate) fn outputs_iter(&self) -> impl Iterator<Item = &OutputModel> {
+    pub(crate) fn outputs_iter(&self) -> impl Iterator<Item = &ValueModel> {
         self.methods_iter()
-            .flat_map(|method| method.outputs().iter())
+            .flat_map(|method| method.output_values().iter())
+    }
+
+    /// Returns the tokens for the use statements which bring the types used inside spawn functions
+    /// into scope.
+    pub(crate) fn use_statements(&self) -> &TokenStream {
+        &self.use_statements
     }
 
+    /// The name of the message enum used by this protocol.
     pub(crate) fn msg_enum_ident(&self) -> &Ident {
         &self.msg_enum_name
     }
 
+    /// The name of the message kinds enum used by this protocol.
     pub(crate) fn msg_enum_kinds_ident(&self) -> &Ident {
         &self.msg_enum_kinds_name
     }
 
+    /// The [Ident] containing [End::ident].
     pub(crate) fn end_ident(&self) -> &Ident {
         &self.end_ident
     }
 
-    pub(crate) fn mailbox_param(&self) -> &Ident {
-        &self.mailbox_param
+    /// Returns a token stream which references the [End] struct using a fully qualified path.
+    #[allow(dead_code)]
+    pub(crate) fn end_struct(&self) -> &TokenStream {
+        &self.end_struct
     }
 
+    /// The name of the [btrun::ActorId] parameter in an actor closure.
     pub(crate) fn actor_id_param(&self) -> &Ident {
         &self.actor_id_param
     }
 
+    /// The name of the `&'static `[Runtime] parameter in an actor closure.
     pub(crate) fn runtime_param(&self) -> &Ident {
         &self.runtime_param
     }
 
+    /// The name of the variable used to hold the `from` field of a [btrun::Envelope].
     #[allow(clippy::wrong_self_convention)]
     pub(crate) fn from_ident(&self) -> &Ident {
         &self.from_ident
     }
 
+    /// The name of the variable used to hold the `reply` field from a [btrun::Envelope].
     pub(crate) fn reply_ident(&self) -> &Ident {
         &self.reply_ident
     }
 
+    /// The name of the variable used to hold the `msg` field from a [btrun::Envelope].
     pub(crate) fn msg_ident(&self) -> &Ident {
         &self.msg_ident
     }
 
+    /// The name of the local variable in an actor closure used to hold the actor's name.
     pub(crate) fn actor_name_ident(&self) -> &Ident {
         &self.actor_name_ident
     }
 
+    /// The identifier for the variable holding the initial state of an actor.
     pub(crate) fn init_state_var(&self) -> &Ident {
         &self.init_state_var
     }
 
+    /// The identifier for the `Init` type parameter used in client enums, handles, and spawn
+    /// functions.
+    pub(crate) fn init_type_param(&self) -> &Ident {
+        &self.init_state_type_param
+    }
+
+    /// The type parameter for client handles which indicates the handle's current state.
+    pub(crate) fn state_type_param(&self) -> &Ident {
+        &self.state_type_param
+    }
+
+    /// The type parameter for client handles which indicates the new state the handle is
+    /// transitioning to.
+    pub(crate) fn new_state_type_param(&self) -> &Ident {
+        &self.new_state_type_param
+    }
+
+    /// The name of the method used to transition client handles to a new type state.
+    pub(crate) fn new_state_method(&self) -> &Ident {
+        &self.new_state_method
+    }
+
+    /// The identifier of variable used to hold the current state of an actor, and the field of
+    /// a client handle which holds the current state.
+    pub(crate) fn state_var(&self) -> &Ident {
+        &self.state_var
+    }
+
+    /// Returns the name of the field used to hold the shared state in a client handle.
+    pub(crate) fn state_field(&self) -> &Ident {
+        &self.state_var
+    }
+
+    /// Returns an iterator over the [Ident]s for each of the states in [actor].
+    pub(crate) fn state_idents<'a>(
+        &'a self,
+        actor: &'a ActorModel,
+    ) -> impl Iterator<Item = &'a Ident> {
+        actor
+            .states()
+            .values()
+            .map(|state| state.name())
+            .chain(std::iter::once(&self.end_ident))
+    }
+
     fn get_actor<'a>(&'a self, actor_name: &Ident) -> &'a ActorModel {
         self.actors
             .get(actor_name)
             .unwrap_or_else(|| panic!("Invalid actor name: '{actor_name}'"))
     }
 
-    fn get_state<'a>(&'a self, state_name: &Ident) -> &'a StateModel {
+    pub(crate) fn get_state<'a>(&'a self, state_name: &Ident) -> &'a StateModel {
         let actor_name = self.actor_lookup().actor_with_state(state_name);
         let actor = self.get_actor(actor_name);
         actor
@@ -276,6 +355,24 @@ impl ProtocolModel {
             type_params,
         }
     }
+
+    /// Returns an iterator of the names of the states which the given state transitions to.
+    pub(crate) fn next_states<'a>(
+        &'a self,
+        state: &'a StateModel,
+    ) -> impl Iterator<Item = &'a ValueModel> {
+        state
+            .methods()
+            .values()
+            // The first output of a method is the state this state transitions to.
+            .flat_map(|method| method.output_values().first().zip(Some(method.name())))
+            .filter(|(output, method_name)| {
+                let state_trait = output.kind().state_trait()
+                    .unwrap_or_else(|| panic!("The first output of method {method_name} in state {} was not the correct kind.", state.name()));
+                state_trait.as_ref() != End::ident()
+            })
+            .map(|(output, _)| output)
+    }
 }
 
 pub(crate) struct TypeParamInfo<'a> {
@@ -320,6 +417,7 @@ pub(crate) struct ActorModel {
     state_enum_ident: Ident,
     states: HashMap<Rc<Ident>, StateModel>,
     spawn_function_ident: Option<Ident>,
+    handle_struct_ident: Option<Ident>,
 }
 
 impl ActorModel {
@@ -353,6 +451,7 @@ impl ActorModel {
         Ok(Self {
             state_enum_ident: Self::make_state_enum_ident(actor_name),
             spawn_function_ident: Self::make_spawn_function_ident(kind, actor_name),
+            handle_struct_ident: Self::make_handle_struct_ident(kind, actor_name),
             def,
             kind,
             states,
@@ -372,6 +471,14 @@ impl ActorModel {
         }
     }
 
+    fn make_handle_struct_ident(kind: ActorKind, actor_name: &Ident) -> Option<Ident> {
+        if let ActorKind::Client = kind {
+            Some(format_ident!("{}Handle", actor_name.snake_to_pascal()))
+        } else {
+            None
+        }
+    }
+
     pub(crate) fn def(&self) -> &ActorDef {
         &self.def
     }
@@ -402,6 +509,10 @@ impl ActorModel {
     pub(crate) fn spawn_function_ident(&self) -> Option<&Ident> {
         self.spawn_function_ident.as_ref()
     }
+
+    pub(crate) fn handle_struct_ident(&self) -> Option<&Ident> {
+        self.handle_struct_ident.as_ref()
+    }
 }
 
 pub(crate) struct StateModel {
@@ -456,7 +567,7 @@ impl StateModel {
 
     fn out_states_and_assoc_types(&self) -> impl Iterator<Item = (&Ident, &Ident)> {
         self.methods().values().flat_map(|method| {
-            method.outputs().iter().flat_map(|output| {
+            method.output_values().iter().flat_map(|output| {
                 output
                     .kind()
                     .state_trait()
@@ -477,27 +588,31 @@ impl GetSpan for StateModel {
 pub(crate) struct MethodModel {
     def: Rc<Transition>,
     name: Rc<Ident>,
-    inputs: Vec<InputModel>,
-    outputs: Vec<OutputModel>,
+    handle_name: Option<Ident>,
+    inputs: Vec<ValueModel>,
+    outputs: Vec<ValueModel>,
     future: Ident,
 }
 
 impl MethodModel {
     fn new(def: Rc<Transition>, messages: &MsgLookup, part_of_client: bool) -> syn::Result<Self> {
-        let name = Rc::new(Self::new_name(def.as_ref())?);
+        let (name, client_handle_name) = Self::new_name(def.as_ref())?;
+        let name = Rc::new(name);
         let type_prefix = name.snake_to_pascal();
         Ok(Self {
             name,
-            inputs: Self::new_inputs(def.as_ref(), messages, part_of_client),
-            outputs: Self::new_outputs(def.as_ref(), &type_prefix, messages, part_of_client),
+            handle_name: client_handle_name,
+            inputs: Self::make_inputs(def.as_ref(), &type_prefix, messages, part_of_client),
+            outputs: Self::make_outputs(def.as_ref(), &type_prefix, messages, part_of_client),
             future: format_ident!("{type_prefix}Fut"),
             def,
         })
     }
 
-    fn new_name(def: &Transition) -> syn::Result<Ident> {
-        let name = if let Some(msg) = def.in_msg() {
-            format_ident!("handle_{}", msg.variant().pascal_to_snake())
+    fn new_name(def: &Transition) -> syn::Result<(Ident, Option<Ident>)> {
+        let pair = if let Some(msg) = def.in_msg() {
+            let name = format_ident!("handle_{}", msg.variant().pascal_to_snake());
+            (name, None)
         } else {
             let mut dests = def.out_msgs.as_ref().iter();
             let mut msg_names = String::new();
@@ -513,83 +628,61 @@ impl MethodModel {
                 msg_names.push('_');
                 msg_names.push_str(dest.msg.variant().pascal_to_snake().as_str());
             }
-            format_ident!("on_send_{msg_names}")
+            let name = format_ident!("on_send_{msg_names}");
+            let handle_name = format_ident!("send_{msg_names}");
+            (name, Some(handle_name))
         };
-        Ok(name)
+        Ok(pair)
     }
 
-    fn new_inputs(def: &Transition, messages: &MsgLookup, part_of_client: bool) -> Vec<InputModel> {
+    fn make_inputs(
+        def: &Transition,
+        type_prefix: &str,
+        messages: &MsgLookup,
+        part_of_client: bool,
+    ) -> Vec<ValueModel> {
         let mut inputs = Vec::new();
-        let arg_kind = if def.not_receiving() {
-            InputKind::ByMutRef
-        } else {
-            InputKind::ByValue
-        };
         if let Some(in_msg) = def.in_msg() {
-            let msg_info = messages.lookup(in_msg);
-            inputs.push(InputModel::new(
-                msg_info.msg_name().clone(),
-                msg_info.msg_type.clone(),
-                arg_kind,
-            ))
+            let kind = ValueKind::Msg {
+                def: in_msg.clone(),
+            };
+            inputs.push(ValueModel::new(kind, type_prefix));
         }
         if part_of_client {
-            for out_msg in def.out_msgs.as_ref().iter() {
-                let msg_info = messages.lookup(&out_msg.msg);
-                inputs.push(InputModel::new(
-                    msg_info.msg_name().clone(),
-                    msg_info.msg_type.clone(),
-                    arg_kind,
-                ))
+            for dest in def.out_msgs.as_ref().iter() {
+                let kind = ValueKind::new_dest(dest.clone(), messages, part_of_client);
+                inputs.push(ValueModel::new(kind, type_prefix));
             }
         }
         inputs
     }
 
-    /// Returns the input associated with the message this method is handling, or [None] if this
-    /// method is not handling a message.
-    pub(crate) fn msg_received_input(&self) -> Option<&InputModel> {
-        if self.def.in_msg().is_some() {
-            let input_model = self.inputs().get(0).unwrap_or_else(|| {
-                panic!(
-                    "Method {} had no inputs despite handling a message.",
-                    self.name()
-                )
-            });
-            Some(input_model)
-        } else {
-            None
-        }
-    }
-
-    fn new_outputs(
+    fn make_outputs(
         def: &Transition,
         type_prefix: &str,
         messages: &MsgLookup,
         part_of_client: bool,
-    ) -> Vec<OutputModel> {
+    ) -> Vec<ValueModel> {
         let mut outputs = Vec::new();
         for state in def.out_states.as_ref().iter() {
-            outputs.push(OutputModel::new(
-                OutputKind::State { def: state.clone() },
-                type_prefix,
-            ));
+            let kind = ValueKind::State { def: state.clone() };
+            outputs.push(ValueModel::new(kind, type_prefix));
         }
-        for dest in def.out_msgs.as_ref().iter() {
-            let msg_info = messages.lookup(&dest.msg);
-            outputs.push(OutputModel::new(
-                OutputKind::Msg {
-                    def: dest.clone(),
-                    msg_type: msg_info.msg_type.clone(),
-                    is_call: msg_info.is_call(),
-                    part_of_client,
-                },
-                type_prefix,
-            ))
+        if !part_of_client {
+            for dest in def.out_msgs.as_ref().iter() {
+                let kind = ValueKind::new_dest(dest.clone(), messages, part_of_client);
+                outputs.push(ValueModel::new(kind, type_prefix));
+            }
         }
         outputs
     }
 
+    /// Returns the input associated with the message this method is handling, or [None] if this
+    /// method is not handling a message.
+    pub(crate) fn msg_received_input(&self) -> Option<&Message> {
+        self.def.in_msg().map(|rc| rc.as_ref())
+    }
+
     pub(crate) fn def(&self) -> &Transition {
         self.def.as_ref()
     }
@@ -598,105 +691,86 @@ impl MethodModel {
         &self.name
     }
 
-    pub(crate) fn inputs(&self) -> &Vec<InputModel> {
+    pub(crate) fn inputs(&self) -> &Vec<ValueModel> {
         &self.inputs
     }
 
-    pub(crate) fn outputs(&self) -> &Vec<OutputModel> {
+    pub(crate) fn output_values(&self) -> &Vec<ValueModel> {
         &self.outputs
     }
 
     pub(crate) fn future(&self) -> &Ident {
         &self.future
     }
-}
 
-impl GetSpan for MethodModel {
-    fn span(&self) -> Span {
-        self.def.span()
+    /// The name of this method in a client handle, if this method is part of a client actor.
+    pub(crate) fn handle_name(&self) -> Option<&Ident> {
+        self.handle_name.as_ref()
     }
-}
-
-#[cfg_attr(test, derive(Debug))]
-#[derive(Clone)]
-enum InputKind {
-    ByValue,
-    ByMutRef,
-}
 
-impl Copy for InputKind {}
-
-#[cfg_attr(test, derive(Debug))]
-pub(crate) struct InputModel {
-    name: Ident,
-    msg_name: Rc<Ident>,
-    arg_type: Rc<TokenStream>,
-    arg_kind: InputKind,
-}
-
-impl InputModel {
-    fn new(msg_name: Rc<Ident>, arg_type: Rc<TokenStream>, arg_kind: InputKind) -> Self {
-        let name = format_ident!("{}_arg", msg_name.pascal_to_snake());
-        Self {
-            name,
-            msg_name,
-            arg_type,
-            arg_kind,
-        }
+    /// Returns the output for the state this method transitions into.
+    pub(crate) fn next_state(&self) -> &ValueModel {
+        self.output_values().get(0).unwrap()
     }
 
-    /// Returns the name of the message this input is for.
-    pub(crate) fn msg_name(&self) -> &Ident {
-        self.msg_name.as_ref()
+    /// Returns an iterator over the output variables returned by this method.
+    pub(crate) fn output_vars(&self) -> impl Iterator<Item = &'_ Ident> {
+        self.output_values()
+            .iter()
+            .flat_map(|output| output.output_var_name())
     }
 }
 
-impl ToTokens for InputModel {
-    fn to_tokens(&self, tokens: &mut TokenStream) {
-        let name = &self.name;
-        let arg_type = self.arg_type.as_ref();
-        let modifier = match self.arg_kind {
-            InputKind::ByValue => quote! {},
-            InputKind::ByMutRef => quote! { &mut },
-        };
-        tokens.extend(quote! { #name : #modifier #arg_type })
+impl GetSpan for MethodModel {
+    fn span(&self) -> Span {
+        self.def.span()
     }
 }
 
 #[cfg_attr(test, derive(Debug))]
-pub(crate) struct OutputModel {
-    kind: OutputKind,
+pub(crate) struct ValueModel {
+    kind: ValueKind,
     type_name: Option<TokenStream>,
     assoc_type: Option<Ident>,
     decl: Option<TokenStream>,
     var_name: Ident,
 }
 
-impl OutputModel {
-    fn new(kind: OutputKind, type_prefix: &str) -> Self {
+impl ValueModel {
+    fn new(kind: ValueKind, type_prefix: &str) -> Self {
         let (decl, type_name, assoc_type) = match &kind {
-            OutputKind::State { def, .. } => {
+            ValueKind::Msg { def, .. } => {
+                let decl = None;
+                let msg_type = def.msg_type.as_ref();
+                let type_name = Some(quote! { #msg_type });
+                let assoc_type = None;
+                (decl, type_name, assoc_type)
+            }
+            ValueKind::State { def, .. } => {
                 let state_trait = def.state_trait.as_ref();
                 if state_trait == End::ident() {
+                    let decl = None;
                     let end_ident = format_ident!("{}", End::ident());
-                    (None, Some(quote! { ::btrun::model::#end_ident }), None)
+                    let type_name = Some(quote! { ::btrun::model::#end_ident });
+                    let assoc_type = None;
+                    (decl, type_name, assoc_type)
                 } else {
-                    let assoc_type = format_ident!("{type_prefix}{}", state_trait);
-                    (
-                        Some(quote! { type  #assoc_type: #state_trait; }),
-                        Some(quote! { Self::#assoc_type }),
-                        Some(assoc_type),
-                    )
+                    let assoc_type_ident = format_ident!("{type_prefix}{}", state_trait);
+                    let decl = Some(quote! { type  #assoc_type_ident: #state_trait; });
+                    let type_name = Some(quote! { Self::#assoc_type_ident });
+                    let assoc_type = Some(assoc_type_ident);
+                    (decl, type_name, assoc_type)
                 }
             }
-            OutputKind::Msg {
+            ValueKind::Dest {
                 msg_type,
+                reply_type,
                 part_of_client,
-                is_call,
                 ..
             } => {
+                let decl = None;
                 let type_name = if *part_of_client {
-                    if *is_call {
+                    if reply_type.is_some() {
                         Some(quote! {
                             <#msg_type as ::btrun::model::CallMsg>::Reply
                         })
@@ -706,7 +780,8 @@ impl OutputModel {
                 } else {
                     Some(quote! { #msg_type })
                 };
-                (None, type_name, None)
+                let assoc_type = None;
+                (decl, type_name, assoc_type)
             }
         };
         Self {
@@ -718,6 +793,11 @@ impl OutputModel {
         }
     }
 
+    fn span(&self) -> Span {
+        self.kind.span()
+    }
+
+    /// The code for specifying the type of this input.
     pub(crate) fn type_name(&self) -> Option<&TokenStream> {
         self.type_name.as_ref()
     }
@@ -726,10 +806,12 @@ impl OutputModel {
         self.decl.as_ref()
     }
 
-    pub(crate) fn kind(&self) -> &OutputKind {
+    pub(crate) fn kind(&self) -> &ValueKind {
         &self.kind
     }
 
+    /// Returns the associated type for this output. If this output not a state, or is the end
+    /// state, then `None` is returned.
     pub(crate) fn assoc_type(&self) -> Option<&Ident> {
         self.assoc_type.as_ref()
     }
@@ -737,29 +819,105 @@ impl OutputModel {
     pub(crate) fn var_name(&self) -> &Ident {
         &self.var_name
     }
+
+    /// If this output actually appears in the tuple returned by its method, then its variable name
+    /// is returned. Otherwise, `None` is returned.
+    ///
+    /// An output of a transition is not actually be an output of the corresponding trait method in
+    /// the case of a method in a client actor.
+    pub(crate) fn output_var_name(&self) -> Option<&Ident> {
+        if self.type_name.is_some() {
+            Some(&self.var_name)
+        } else {
+            None
+        }
+    }
+
+    /// Returns the token for this input when it appears in a client handle method.
+    pub(crate) fn as_handle_param(&self) -> TokenStream {
+        let name = &self.var_name;
+        if let ValueKind::Dest { msg_type, .. } = &self.kind {
+            quote_spanned! {self.span()=> mut #name: #msg_type }
+        } else {
+            quote_spanned! {self.span() => }
+        }
+    }
+
+    #[allow(dead_code)]
+    pub(crate) fn as_method_call(&self) -> TokenStream {
+        if let ValueKind::Msg { .. } | ValueKind::Dest { .. } = &self.kind {
+            let var_name = &self.var_name;
+            quote_spanned! {self.span()=> #var_name }
+        } else {
+            quote_spanned! {self.span()=> }
+        }
+    }
+
+    pub(crate) fn in_method_decl(&self) -> TokenStream {
+        let var_name = &self.var_name;
+        match &self.kind {
+            ValueKind::Msg { def, .. } => {
+                let msg_type = def.msg_type.as_ref();
+                quote! { #var_name: #msg_type }
+            }
+            ValueKind::State { .. } => quote_spanned! {self.span()=> },
+            // Dest values only ever occur in the inputs of clients. In client handles, only call
+            // replies are passed in.
+            ValueKind::Dest { reply_type, .. } => {
+                if let Some(reply_type) = reply_type {
+                    quote! { #var_name: #reply_type }
+                } else {
+                    quote! {}
+                }
+            }
+        }
+    }
 }
 
 #[cfg_attr(test, derive(Debug))]
-pub(crate) enum OutputKind {
-    State {
-        def: Rc<State>,
-    },
-    Msg {
-        #[allow(dead_code)]
+pub(crate) enum ValueKind {
+    /// Represents a value which is passing in a message, and so always occurs in an input position.
+    Msg { def: Rc<Message> },
+    /// Represents a value which is passing out a state, and so always occurs in an output position.
+    State { def: Rc<State> },
+    /// Represents a value which is sending a message to another actor, and so is in the input
+    /// position for client actors, but in the output position for server actors.
+    Dest {
         def: Rc<Dest>,
         msg_type: Rc<TokenStream>,
-        is_call: bool,
+        reply_type: Option<Rc<TokenStream>>,
         part_of_client: bool,
     },
 }
 
-impl OutputKind {
+impl ValueKind {
     fn var_name(&self) -> Ident {
         let ident = match self {
+            Self::Msg { def, .. } => def.msg_type.as_ref(),
             Self::State { def, .. } => def.state_trait.as_ref(),
-            Self::Msg { def, .. } => def.msg.msg_type.as_ref(),
+            Self::Dest { def, .. } => def.state.state_ref().state_trait.as_ref(),
         };
-        format_ident!("{}_out", ident.pascal_to_snake())
+        format_ident!("{}_var", ident.pascal_to_snake())
+    }
+
+    fn new_dest(def: Rc<Dest>, msg_lookup: &MsgLookup, part_of_client: bool) -> Self {
+        let msg_info = msg_lookup.lookup(&def.msg);
+        let msg_type = msg_info.msg_type().clone();
+        let reply_type = msg_info.reply().map(|reply| reply.msg_type().clone());
+        Self::Dest {
+            def,
+            msg_type,
+            reply_type,
+            part_of_client,
+        }
+    }
+
+    fn span(&self) -> Span {
+        match self {
+            Self::Msg { def, .. } => def.span(),
+            Self::State { def, .. } => def.span(),
+            Self::Dest { def, .. } => def.span(),
+        }
     }
 
     pub(crate) fn state_trait(&self) -> Option<&Rc<Ident>> {
@@ -1212,6 +1370,8 @@ mod tests {
     #[test]
     fn reply_is_marked_in_output() {
         const MSG: &str = "Ping";
+        let msg_ident = format_ident!("{MSG}");
+        let expected = quote! { < #msg_ident as :: btrun :: model :: CallMsg > :: Reply };
         let input = Protocol::new(
             NameDef::new("ReplyTest"),
             [
@@ -1237,32 +1397,21 @@ mod tests {
                         Message::new(MSG, true, []),
                     )],
                 ),
-                Transition::new(
-                    State::new("Waiting", []),
-                    Some(Message::new(MSG, true, [])),
-                    [State::new(End::ident(), [])],
-                    [],
-                ),
             ],
         );
 
         let actual = ProtocolModel::new(input).unwrap();
 
-        let outputs: Vec<_> = actual
-            .outputs_iter()
-            .map(|output| {
-                if let OutputKind::Msg { is_call, .. } = output.kind {
-                    Some(is_call)
-                } else {
-                    None
-                }
-            })
-            .filter(|x| x.is_some())
-            .map(|x| x.unwrap())
-            .collect();
-        assert_eq!(2, outputs.len());
-        assert_eq!(1, outputs.iter().filter(|is_reply| **is_reply).count());
-        assert_eq!(1, outputs.iter().filter(|is_reply| !*is_reply).count());
+        let mut msg_types = actual.outputs_iter().flat_map(|output| {
+            if let ValueKind::Dest { msg_type, .. } = &output.kind {
+                Some(msg_type)
+            } else {
+                None
+            }
+        });
+        let msg_type = msg_types.next().unwrap();
+        assert!(msg_types.next().is_none());
+        assert_eq!(expected.to_string(), msg_type.to_string());
     }
 
     fn simple_client_server_proto() -> Protocol {

+ 7 - 5
crates/btproto/src/parsing.rs

@@ -333,11 +333,6 @@ impl Transition {
     pub(crate) fn in_msg(&self) -> Option<&Rc<Message>> {
         self.in_msg.as_ref().map(|(_, msg)| msg)
     }
-
-    /// Returns true if and only if this [Transition] is not receiving a message.
-    pub(crate) fn not_receiving(&self) -> bool {
-        self.in_msg.is_none()
-    }
 }
 
 #[cfg(test)]
@@ -596,6 +591,13 @@ pub(crate) enum DestinationState {
 impl DestinationState {
     const NO_STATE_ERR: &str = "expected destination state";
     const MULTI_STATE_ERR: &str = "only one destination state is allowed";
+
+    /// Returns a reference to the [State] in this value.
+    pub(crate) fn state_ref(&self) -> &State {
+        match self {
+            Self::Individual(state) | Self::Service(state) => state,
+        }
+    }
 }
 
 impl Parse for DestinationState {

+ 36 - 28
crates/btproto/tests/protocol_tests.rs

@@ -42,9 +42,9 @@ fn minimal_syntax() {
         None => (),
     }
 
-    struct ServerState;
+    struct ServerImpl;
 
-    impl Server for ServerState {
+    impl Server for ServerImpl {
         actor_name!("minimal_server");
 
         type HandleMsgFut = Ready<TransResult<Self, End>>;
@@ -53,13 +53,13 @@ fn minimal_syntax() {
         }
     }
 
-    struct ClientState;
+    struct ClientImpl;
 
-    impl Client for ClientState {
+    impl Client for ClientImpl {
         actor_name!("minimal_client");
 
         type OnSendMsgFut = Ready<TransResult<Self, End>>;
-        fn on_send_msg(self, _msg: &mut Msg) -> Self::OnSendMsgFut {
+        fn on_send_msg(self) -> Self::OnSendMsgFut {
             ready(TransResult::Ok(End))
         }
     }
@@ -82,9 +82,9 @@ fn reply() {
         None => (),
     }
 
-    struct ListeningState;
+    struct ListeningImpl;
 
-    impl Listening for ListeningState {
+    impl Listening for ListeningImpl {
         actor_name!("reply_server");
 
         type HandlePingListening = Self;
@@ -94,15 +94,15 @@ fn reply() {
         }
     }
 
-    struct ClientState;
+    struct ClientImpl;
 
-    impl Client for ClientState {
+    impl Client for ClientImpl {
         actor_name!("reply_client");
 
         type OnSendPingClient = Self;
-        type OnSendPingFut = Ready<TransResult<Self, (Self, <Ping as CallMsg>::Reply)>>;
-        fn on_send_ping(self, _ping: &mut Ping) -> Self::OnSendPingFut {
-            ready(TransResult::Ok((self, ())))
+        type OnSendPingFut = Ready<TransResult<Self, Self>>;
+        fn on_send_ping(self, _ping: <Ping as CallMsg>::Reply) -> Self::OnSendPingFut {
+            ready(TransResult::Ok(self))
         }
     }
 }
@@ -129,46 +129,54 @@ fn client_callback() {
     match msg {
         Some(ClientCallbackMsgs::Register(msg)) => assert_type!(msg, Register),
         Some(ClientCallbackMsgs::Completed(msg)) => assert_type!(msg, Completed),
-        _ => (),
+        None => (),
+    }
+
+    let client_state: Option<ClientState<UnregisteredImpl>> = None;
+    match client_state {
+        Some(ClientState::Unregistered(state)) => assert_type!(state, UnregisteredImpl),
+        Some(ClientState::Registered(state)) => assert_type!(state, RegisteredImpl),
+        Some(ClientState::End(state)) => assert_type!(state, End),
+        None => (),
     }
 
-    struct UnregisteredState;
+    struct UnregisteredImpl;
 
-    impl Unregistered for UnregisteredState {
+    impl Unregistered for UnregisteredImpl {
         actor_name!("callback_client");
 
-        type OnSendRegisterRegistered = RegisteredState;
+        type OnSendRegisterRegistered = RegisteredImpl;
         type OnSendRegisterFut = Ready<TransResult<Self, Self::OnSendRegisterRegistered>>;
-        fn on_send_register(self, _arg: &mut Register) -> Self::OnSendRegisterFut {
-            ready(TransResult::Ok(RegisteredState))
+        fn on_send_register(self) -> Self::OnSendRegisterFut {
+            ready(TransResult::Ok(RegisteredImpl))
         }
     }
 
-    struct RegisteredState;
+    struct RegisteredImpl;
 
-    impl Registered for RegisteredState {
+    impl Registered for RegisteredImpl {
         type HandleCompletedFut = Ready<TransResult<Self, End>>;
         fn handle_completed(self, _arg: Completed) -> Self::HandleCompletedFut {
             ready(TransResult::Ok(End))
         }
     }
 
-    struct ListeningState;
+    struct ListeningImpl;
 
-    impl Listening for ListeningState {
+    impl Listening for ListeningImpl {
         actor_name!("callback_server");
 
-        type HandleRegisterListening = ListeningState;
-        type HandleRegisterWorking = WorkingState;
-        type HandleRegisterFut = Ready<TransResult<Self, (ListeningState, WorkingState)>>;
+        type HandleRegisterListening = ListeningImpl;
+        type HandleRegisterWorking = WorkingImpl;
+        type HandleRegisterFut = Ready<TransResult<Self, (ListeningImpl, WorkingImpl)>>;
         fn handle_register(self, _arg: Register) -> Self::HandleRegisterFut {
-            ready(TransResult::Ok((self, WorkingState)))
+            ready(TransResult::Ok((self, WorkingImpl)))
         }
     }
 
-    struct WorkingState;
+    struct WorkingImpl;
 
-    impl Working for WorkingState {
+    impl Working for WorkingImpl {
         actor_name!("callback_worker");
 
         type OnSendCompletedFut = Ready<TransResult<Self, (End, Completed)>>;

+ 18 - 1
crates/btrun/src/model.rs

@@ -6,7 +6,7 @@ use btlib::{bterr, BlockPath, Result};
 use btserde::{field_helpers::smart_ptr, from_slice, write_to};
 use serde::{de::DeserializeOwned, Deserialize, Serialize};
 use tokio::{
-    sync::{mpsc, oneshot, Mutex},
+    sync::{mpsc, oneshot},
     task::AbortHandle,
 };
 use uuid::Uuid;
@@ -14,6 +14,7 @@ use uuid::Uuid;
 use crate::{WireEnvelope, WireReply};
 
 pub use once_cell::sync::Lazy;
+pub use tokio::sync::Mutex;
 
 /// The return type of actor closures.
 pub type ActorResult = std::result::Result<ActorId, ActorError>;
@@ -37,6 +38,22 @@ pub enum TransResult<From, To> {
     Fatal { err: btlib::Error },
 }
 
+impl<From, To> TransResult<From, To> {
+    /// If this value is `Ok` then the `To` value it contains is returned. Otherwise, this method
+    /// panics.
+    pub fn unwrap(self) -> To {
+        match self {
+            Self::Ok(to) => to,
+            Self::Abort { err, .. } => {
+                panic!("Called `TransResult::unwrap()` on an `Abort` value: {err}")
+            }
+            Self::Fatal { err, .. } => {
+                panic!("Called `TransResult::unwrap()` on a `Fatal` value: {err}")
+            }
+        }
+    }
+}
+
 #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
 /// Specifies a kind of transition, either a `Send` or a `Receive`.
 pub enum TransKind {

+ 142 - 96
crates/btrun/tests/runtime_tests.rs

@@ -58,59 +58,78 @@ mod ping_pong {
         }
     }
 
-    struct ClientHandle<T: Client> {
+    struct ClientHandleManual<T: Client> {
         state: Option<PingClientState<T>>,
         client_name: ActorName,
         runtime: &'static Runtime,
     }
 
-    impl<T: Client> ClientHandle<T> {
-        async fn send_ping(&mut self, mut msg: Ping, service: ServiceAddr) -> Result<PingReply> {
-            let state = self
-                .state
-                .take()
-                .ok_or_else(|| bterr!("State was not returned."))?;
-            let (new_state, result) = match state {
-                PingClientState::Client(state) => match state.on_send_ping(&mut msg).await {
-                    TransResult::Ok((new_state, _)) => {
-                        let new_state = PingClientState::End(new_state);
-                        let result = self
-                            .runtime
-                            .call_service(
-                                service,
-                                self.client_name.clone(),
-                                PingProtocolMsgs::Ping(msg),
-                            )
-                            .await;
-                        (new_state, result)
-                    }
-                    TransResult::Abort { from, err } => {
-                        let new_state = PingClientState::Client(from);
-                        (new_state, Err(err))
+    impl<T: Client> ClientHandleManual<T> {
+        async fn send_ping(
+            mut self,
+            msg: Ping,
+            service: ServiceAddr,
+        ) -> TransResult<Self, ClientHandleManual<T>> {
+            let state = if let Some(state) = self.state.take() {
+                state
+            } else {
+                return TransResult::Abort {
+                    from: self,
+                    err: bterr!("The shared state was not returned."),
+                };
+            };
+            match state {
+                PingClientState::Client(state) => {
+                    let result = self
+                        .runtime
+                        .call_service(
+                            service,
+                            self.client_name.clone(),
+                            PingProtocolMsgs::Ping(msg),
+                        )
+                        .await;
+                    let reply_enum = match result {
+                        Ok(reply_enum) => reply_enum,
+                        Err(err) => {
+                            self.state = Some(PingClientState::Client(state));
+                            return TransResult::Abort { from: self, err };
+                        }
+                    };
+                    if let PingProtocolMsgs::PingReply(reply) = reply_enum {
+                        match state.on_send_ping(reply).await {
+                            TransResult::Ok(new_state) => {
+                                self.state = Some(PingClientState::End(new_state));
+                                TransResult::Ok(self)
+                            }
+                            TransResult::Abort { from, err } => {
+                                self.state = Some(PingClientState::Client(from));
+                                TransResult::Abort { from: self, err }
+                            }
+                            TransResult::Fatal { err } => return TransResult::Fatal { err },
+                        }
+                    } else {
+                        TransResult::Abort {
+                            from: self,
+                            err: bterr!("Unexpected reply type."),
+                        }
                     }
-                    TransResult::Fatal { err } => return Err(err),
-                },
+                }
                 state => {
-                    let result = Err(bterr!("Can't send Ping in state {}.", state.name()));
-                    (state, result)
+                    let err = bterr!("Can't send Ping in state {}.", state.name());
+                    self.state = Some(state);
+                    TransResult::Abort { from: self, err }
                 }
-            };
-            self.state = Some(new_state);
-            let reply = result?;
-            match reply {
-                PingProtocolMsgs::PingReply(reply) => Ok(reply),
-                msg => Err(bterr!(
-                    "Unexpected message type sent in reply: {}",
-                    msg.name()
-                )),
             }
         }
     }
 
-    async fn spawn_client_manual<T: Client>(init: T, runtime: &'static Runtime) -> ClientHandle<T> {
+    async fn spawn_client_manual<T: Client>(
+        init: T,
+        runtime: &'static Runtime,
+    ) -> ClientHandleManual<T> {
         let state = Some(PingClientState::Client(init));
         let client_name = runtime.spawn(None, do_nothing_actor).await.unwrap();
-        ClientHandle {
+        ClientHandleManual {
             state,
             client_name,
             runtime,
@@ -166,8 +185,6 @@ mod ping_pong {
                                     let replier = replier
                                         .ok_or_else(|| bterr!("Reply has already been sent."))
                                         .unwrap();
-                                    //let replier =
-                                    //    replier.expect("The reply has already been sent.");
                                     if let Err(_) = replier.send(PingProtocolMsgs::PingReply(reply))
                                     {
                                         return Err(ActorError::new(
@@ -249,24 +266,24 @@ mod ping_pong {
     #[derive(Serialize, Deserialize)]
     pub struct PingReply;
 
-    struct ClientState {
+    struct ClientImpl {
         counter: Arc<AtomicU8>,
     }
 
-    impl ClientState {
+    impl ClientImpl {
         fn new(counter: Arc<AtomicU8>) -> Self {
             counter.fetch_add(1, Ordering::SeqCst);
             Self { counter }
         }
     }
 
-    impl Client for ClientState {
+    impl Client for ClientImpl {
         actor_name!("ping_client");
 
-        type OnSendPingFut = impl Future<Output = TransResult<Self, (End, PingReply)>>;
-        fn on_send_ping(self, _msg: &mut Ping) -> Self::OnSendPingFut {
+        type OnSendPingFut = impl Future<Output = TransResult<Self, End>>;
+        fn on_send_ping(self, _msg: PingReply) -> Self::OnSendPingFut {
             self.counter.fetch_sub(1, Ordering::SeqCst);
-            ready(TransResult::Ok((End, PingReply)))
+            ready(TransResult::Ok(End))
         }
     }
 
@@ -307,14 +324,12 @@ mod ping_pong {
                     .await
                     .unwrap()
             };
-            let mut client_handle =
-                spawn_client_manual(ClientState::new(counter.clone()), &RUNTIME).await;
+            let client_handle =
+                spawn_client_manual(ClientImpl::new(counter.clone()), &RUNTIME).await;
             let service_addr = ServiceAddr::new(service_name, true);
             client_handle.send_ping(Ping, service_addr).await.unwrap();
 
             assert_eq!(0, counter.load(Ordering::SeqCst));
-
-            RUNTIME.deregister(&service_id, None).await.unwrap();
         });
     }
 }
@@ -335,11 +350,8 @@ mod travel_agency {
         Choosing -> Choosing, >service(Listening)!Accept;
         Choosing -> Choosing, >service(Listening)!Reject;
         Listening?Query -> Listening, >Choosing!Query::Reply;
-        Choosing?Query::Reply -> Choosing;
         Listening?Accept -> End, >Choosing!Accept::Reply;
-        Choosing?Accept::Reply -> End;
         Listening?Reject -> End, >Choosing!Reject::Reply;
-        Choosing?Reject::Reply -> End;
     }
 
     #[derive(Serialize, Deserialize)]
@@ -371,7 +383,7 @@ mod client_callback {
 
     use btlib::bterr;
     use once_cell::sync::Lazy;
-    use std::{panic::panic_any, time::Duration};
+    use std::{marker::PhantomData, panic::panic_any, time::Duration};
     use tokio::{sync::oneshot, time::timeout};
 
     #[derive(Serialize, Deserialize)]
@@ -404,7 +416,7 @@ mod client_callback {
 
         type OnSendRegisterRegistered = RegisteredState;
         type OnSendRegisterFut = Ready<TransResult<Self, Self::OnSendRegisterRegistered>>;
-        fn on_send_register(self, _arg: &mut Register) -> Self::OnSendRegisterFut {
+        fn on_send_register(self) -> Self::OnSendRegisterFut {
             ready(TransResult::Ok(RegisteredState {
                 sender: self.sender,
             }))
@@ -462,13 +474,13 @@ mod client_callback {
 
     use ::tokio::sync::Mutex;
 
-    enum ClientState<Init: Unregistered> {
+    enum ClientStateManual<Init: Unregistered> {
         Unregistered(Init),
         Registered(Init::OnSendRegisterRegistered),
         End(End),
     }
 
-    impl<Init: Unregistered> Named for ClientState<Init> {
+    impl<Init: Unregistered> Named for ClientStateManual<Init> {
         fn name(&self) -> Arc<String> {
             static UNREGISTERED_NAME: Lazy<Arc<String>> =
                 Lazy::new(|| Arc::new("Unregistered".into()));
@@ -482,51 +494,79 @@ mod client_callback {
         }
     }
 
-    struct ClientHandle<Init: Unregistered> {
+    struct ClientHandleManual<Init: Unregistered, State> {
         runtime: &'static Runtime,
-        state: Arc<Mutex<Option<ClientState<Init>>>>,
+        state: Arc<Mutex<Option<ClientStateManual<Init>>>>,
         name: ActorName,
+        type_state: PhantomData<State>,
     }
 
-    impl<Init: Unregistered> ClientHandle<Init> {
-        async fn send_register(&self, to: ServiceAddr, mut msg: Register) -> Result<()> {
-            let mut guard = self.state.lock().await;
-            let state = guard
-                .take()
-                .unwrap_or_else(|| panic!("Logic error. The state was not returned."));
-            let new_state = match state {
-                ClientState::Unregistered(state) => match state.on_send_register(&mut msg).await {
-                    TransResult::Ok(new_state) => {
-                        let msg = ClientCallbackMsgs::Register(msg);
-                        self.runtime
-                            .send_service(to, self.name.clone(), msg)
-                            .await?;
-                        ClientState::Registered(new_state)
-                    }
-                    TransResult::Abort { from, err } => {
-                        log::warn!(
-                            "Aborted transition from the {} state: {}",
-                            "Unregistered",
-                            err
-                        );
-                        ClientState::Unregistered(from)
-                    }
-                    TransResult::Fatal { err } => {
-                        return Err(err);
+    impl<Init: Unregistered, State> ClientHandleManual<Init, State> {
+        fn new_type<NewState>(self) -> ClientHandleManual<Init, NewState> {
+            ClientHandleManual {
+                runtime: self.runtime,
+                state: self.state,
+                name: self.name,
+                type_state: PhantomData,
+            }
+        }
+    }
+
+    impl<
+            Init: Unregistered,
+            State: Unregistered<OnSendRegisterRegistered = NewState>,
+            NewState: Registered,
+        > ClientHandleManual<Init, State>
+    {
+        async fn send_register(
+            self,
+            to: ServiceAddr,
+            msg: Register,
+        ) -> Result<ClientHandleManual<Init, NewState>> {
+            {
+                let mut guard = self.state.lock().await;
+                let state = guard
+                    .take()
+                    .unwrap_or_else(|| panic!("Logic error. The state was not returned."));
+                let new_state = match state {
+                    ClientStateManual::Unregistered(state) => {
+                        match state.on_send_register().await {
+                            TransResult::Ok(new_state) => {
+                                let msg = ClientCallbackMsgs::Register(msg);
+                                self.runtime
+                                    .send_service(to, self.name.clone(), msg)
+                                    .await?;
+                                ClientStateManual::Registered(new_state)
+                            }
+                            TransResult::Abort { from, err } => {
+                                log::warn!(
+                                    "Aborted transition from the {} state: {}",
+                                    "Unregistered",
+                                    err
+                                );
+                                ClientStateManual::Unregistered(from)
+                            }
+                            TransResult::Fatal { err } => {
+                                return Err(err);
+                            }
+                        }
                     }
-                },
-                state => state,
-            };
-            *guard = Some(new_state);
-            Ok(())
+                    state => state,
+                };
+                *guard = Some(new_state);
+            }
+            Ok(self.new_type())
         }
     }
 
-    async fn spawn_client_manual<Init>(init: Init, runtime: &'static Runtime) -> ClientHandle<Init>
+    async fn spawn_client_manual<Init>(
+        init: Init,
+        runtime: &'static Runtime,
+    ) -> ClientHandleManual<Init, Init>
     where
         Init: 'static + Unregistered,
     {
-        let state = Arc::new(Mutex::new(Some(ClientState::Unregistered(init))));
+        let state = Arc::new(Mutex::new(Some(ClientStateManual::Unregistered(init))));
         let name = {
             let state = state.clone();
             runtime.spawn(None, move |mut mailbox, actor_id, _| async move {
@@ -537,12 +577,12 @@ mod client_callback {
                     let new_state = match envelope {
                         Envelope::Send { msg, .. } => {
                             match (state, msg) {
-                                (ClientState::Registered(curr_state), ClientCallbackMsgs::Completed(msg)) => {
+                                (ClientStateManual::Registered(curr_state), ClientCallbackMsgs::Completed(msg)) => {
                                     match curr_state.handle_completed(msg).await {
-                                        TransResult::Ok(next) =>  ClientState::<Init>::End(next),
+                                        TransResult::Ok(next) =>  ClientStateManual::<Init>::End(next),
                                         TransResult::Abort { from, err } => {
                                             log::warn!("Aborted transition from the {} state while handling the {} message: {}", "Registered", "Completed", err);
-                                            ClientState::Registered(from)
+                                            ClientStateManual::Registered(from)
                                         }
                                         TransResult::Fatal { err } => {
                                             panic_any(ActorError::new(
@@ -574,14 +614,20 @@ mod client_callback {
                         }))
                     };
                     *guard = Some(new_state);
+                    if let Some(state) = &*guard {
+                        if let ClientStateManual::End(_) = state {
+                            break;
+                        }
+                    }
                 }
                 Ok(actor_id)
             }).await.unwrap()
         };
-        ClientHandle {
+        ClientHandleManual {
             runtime,
             state,
             name,
+            type_state: PhantomData,
         }
     }
 

+ 0 - 1
crates/btsector/src/lib.rs

@@ -63,5 +63,4 @@ protocol! {
     let client = [Client];
     Client -> Client, >service(Listening)!SectorMsg;
     Listening?SectorMsg -> Listening, >Client!SectorMsg::Reply;
-    Client?SectorMsg::Reply -> Client;
 }