17 Komitmen 3066400930 ... 5d2f5b3b90

Pembuat SHA1 Pesan Tanggal
  Matthew Carr 5d2f5b3b90 Improved error handling in btrun. 2 tahun lalu
  Matthew Carr abb47d47c0 * Updated the ping_pong module in btrun. 2 tahun lalu
  Matthew Carr 07ef48cc6c Changed the determination of client actors. 2 tahun lalu
  Matthew Carr b8f54c6aae Modified btproto validation to prevent the definition of 2 tahun lalu
  Matthew Carr 43a8fed38d Refactored btproto to make it easier to evolve. 2 tahun lalu
  Matthew Carr 2fef915a40 * Fixed a bug in the way actor definitions were parsed. 2 tahun lalu
  Matthew Carr 0461b8d0e6 Modified the protocol syntax. 2 tahun lalu
  Matthew Carr 14e08f367a Decoupled btproto::validation tests from the language syntax 2 tahun lalu
  Matthew Carr b77fcd72e1 * Added new validation method to btproto. 2 tahun lalu
  Matthew Carr be2fdf8d81 "Finished" implementing code generation for the protocol macro. 2 tahun lalu
  Matthew Carr 5c949fa300 Started writing the code generation portion of btproto. 2 tahun lalu
  Matthew Carr a53fe62a1a Added code to validate protocol definitions. 2 tahun lalu
  Matthew Carr d900c9f263 Wrote tests for the btproto::parsing crate. 2 tahun lalu
  Matthew Carr 20bf5b6bd2 Implemented a parser for the protocol language using syn. 2 tahun lalu
  Matthew Carr 1870b7ef10 Updated the protocol syntax to include its name and the 2 tahun lalu
  Matthew Carr d626f90ffb Started working out the definitions of the sector 2 tahun lalu
  Matthew Carr f1ae473dd2 Added an empty implementation of the protocol! macro 2 tahun lalu

+ 46 - 12
Cargo.lock

@@ -70,7 +70,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.42",
 ]
 
 [[package]]
@@ -313,6 +313,17 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "btfs"
+version = "0.1.0"
+dependencies = [
+ "btlib",
+ "btproto",
+ "btrun",
+ "btsector",
+ "serde",
+]
+
 [[package]]
 name = "btfsd"
 version = "0.1.0"
@@ -411,6 +422,18 @@ dependencies = [
  "tempdir",
 ]
 
+[[package]]
+name = "btproto"
+version = "0.1.0"
+dependencies = [
+ "btlib",
+ "btrun",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "syn 2.0.42",
+]
+
 [[package]]
 name = "btprovision"
 version = "0.1.0"
@@ -432,6 +455,7 @@ dependencies = [
  "anyhow",
  "btlib",
  "btlib-tests",
+ "btproto",
  "btserde",
  "bttp",
  "bytes",
@@ -446,6 +470,16 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "btsector"
+version = "0.1.0"
+dependencies = [
+ "btlib",
+ "btproto",
+ "btrun",
+ "serde",
+]
+
 [[package]]
 name = "btserde"
 version = "0.1.0"
@@ -1036,7 +1070,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.42",
 ]
 
 [[package]]
@@ -1888,7 +1922,7 @@ dependencies = [
  "proc-macro2",
  "proc-macro2-diagnostics",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.42",
 ]
 
 [[package]]
@@ -1974,7 +2008,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.42",
 ]
 
 [[package]]
@@ -2087,9 +2121,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.63"
+version = "1.0.71"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b368fba921b0dce7e60f5e04ec15e565b3303972b42bcfde1d0713b881959eb"
+checksum = "75cb1540fadbd5b8fbccc4dddad2734eba435053f725621c070711a14bb5f4b8"
 dependencies = [
  "unicode-ident",
 ]
@@ -2102,7 +2136,7 @@ checksum = "606c4ba35817e2922a308af55ad51bab3645b59eae5c570d4a6cf07e36bd493b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.42",
  "version_check",
  "yansi",
 ]
@@ -2176,9 +2210,9 @@ dependencies = [
 
 [[package]]
 name = "quote"
-version = "1.0.29"
+version = "1.0.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "573015e8ab27661678357f27dc26460738fd2b6c86e46f386fde94cb5d913105"
+checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae"
 dependencies = [
  "proc-macro2",
 ]
@@ -2713,9 +2747,9 @@ dependencies = [
 
 [[package]]
 name = "syn"
-version = "2.0.22"
+version = "2.0.42"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2efbeae7acf4eabd6bcdcbd11c92f45231ddda7539edc7806bd1a04a03b24616"
+checksum = "5b7d0a2c048d661a1a59fcd7355baa232f7ed34e0ee4df2eef3c1c1c0d3852d8"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -3119,7 +3153,7 @@ checksum = "3f67b459f42af2e6e1ee213cb9da4dbd022d3320788c3fb3e1b893093f1e45da"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.42",
 ]
 
 [[package]]

+ 13 - 0
crates/btfs/Cargo.toml

@@ -0,0 +1,13 @@
+[package]
+name = "btfs"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+btlib = { path = "../btlib" }
+btrun = { path = "../btrun" }
+btproto = { path = "../btproto" }
+btsector = { path = "../btsector" }
+serde = { version = "^1.0.136", features = ["derive"] }

+ 59 - 0
crates/btfs/src/lib.rs

@@ -0,0 +1,59 @@
+use btproto::protocol;
+use btrun::model::{ActorName, CallMsg, NoReply, SendMsg};
+use btsector::FileId;
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize)]
+pub struct Open {
+    id: FileId,
+}
+
+impl CallMsg for Open {
+    type Reply = ActorName;
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct OpenFile;
+
+impl CallMsg for OpenFile {
+    type Reply = NoReply;
+}
+
+impl SendMsg for OpenFile {}
+
+protocol! {
+    named FsProtocol;
+    let server = [Listening];
+    let client = [Client];
+    let file = [Opened];
+    let file_handle = [FileHandle];
+
+    Client -> Client, >service(Listening)!Query;
+    Listening?Query -> Listening, >Client!Query::Reply;
+
+    Client -> Client, FileHandle[Opened], >service(Listening)!Open;
+    Listening?Open -> Listening, Opened, >Client!Open::Reply[Opened];
+
+    FileHandle[Opened] -> FileHandle[Opened], >Opened!FileOp;
+    Opened?FileOp -> Opened, >FileHandle!FileOp::Reply;
+
+    FileHandle[Opened] -> End, >Opened!Close;
+    Opened?Close -> End;
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct Query;
+
+impl CallMsg for Query {
+    type Reply = ();
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct FileOp;
+
+impl CallMsg for FileOp {
+    type Reply = ();
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct Close;

+ 19 - 0
crates/btproto/Cargo.toml

@@ -0,0 +1,19 @@
+[package]
+name = "btproto"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+proc-macro = true
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+btrun = { path = "../btrun" }
+proc-macro2 = "1.0.71"
+quote = "1.0.33"
+syn = { version = "2.0.42", features = ["extra-traits"] }
+serde = { version = "^1.0.136", features = ["derive"] }
+
+[dev-dependencies]
+btlib = { path = "../btlib" }

+ 163 - 0
crates/btproto/src/case_convert.rs

@@ -0,0 +1,163 @@
+//! Code for converting between different casing disciplines.
+
+use proc_macro2::Ident;
+
+pub(crate) trait CaseConvert {
+    /// Converts a name in snake_case to PascalCase.
+    fn snake_to_pascal(&self) -> String;
+    /// Converts a name in PascalCase to snake_case.
+    fn pascal_to_snake(&self) -> String;
+}
+
+impl CaseConvert for String {
+    fn snake_to_pascal(&self) -> String {
+        let mut pascal = String::with_capacity(self.len());
+        let mut prev_underscore = true;
+        for c in self.chars() {
+            if '_' == c {
+                prev_underscore = true;
+            } else {
+                if prev_underscore {
+                    pascal.extend(c.to_uppercase());
+                } else {
+                    pascal.push(c);
+                }
+                prev_underscore = false;
+            }
+        }
+        pascal
+    }
+
+    fn pascal_to_snake(&self) -> String {
+        let mut snake = String::with_capacity(self.len());
+        let mut prev_lower = false;
+        for c in self.chars() {
+            if c.is_uppercase() {
+                if prev_lower {
+                    snake.push('_');
+                }
+                snake.extend(c.to_lowercase());
+                prev_lower = false;
+            } else {
+                prev_lower = true;
+                snake.push(c);
+            }
+        }
+        snake
+    }
+}
+
+impl CaseConvert for Ident {
+    fn snake_to_pascal(&self) -> String {
+        self.to_string().snake_to_pascal()
+    }
+
+    fn pascal_to_snake(&self) -> String {
+        self.to_string().pascal_to_snake()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn string_snake_to_pascal_multiple_segments() {
+        const EXPECTED: &str = "FirstSecondThird";
+        let input = String::from("first_second_third");
+
+        let actual = input.snake_to_pascal();
+
+        assert_eq!(EXPECTED, actual);
+    }
+
+    #[test]
+    fn string_snake_to_pascal_single_segment() {
+        const EXPECTED: &str = "First";
+        let input = String::from("first");
+
+        let actual = input.snake_to_pascal();
+
+        assert_eq!(EXPECTED, actual);
+    }
+
+    #[test]
+    fn string_snake_to_pascal_empty_string() {
+        const EXPECTED: &str = "";
+        let input = String::from(EXPECTED);
+
+        let actual = input.snake_to_pascal();
+
+        assert_eq!(EXPECTED, actual);
+    }
+
+    #[test]
+    fn string_snake_to_pascal_leading_underscore() {
+        const EXPECTED: &str = "First";
+        let input = String::from("_first");
+
+        let actual = input.snake_to_pascal();
+
+        assert_eq!(EXPECTED, actual);
+    }
+
+    #[test]
+    fn string_snake_to_pascal_leading_underscores() {
+        const EXPECTED: &str = "First";
+        let input = String::from("__first");
+
+        let actual = input.snake_to_pascal();
+
+        assert_eq!(EXPECTED, actual);
+    }
+
+    #[test]
+    fn string_snake_to_pascal_multiple_underscores() {
+        const EXPECTED: &str = "FirstSecondThird";
+        let input = String::from("first__second___third");
+
+        let actual = input.snake_to_pascal();
+
+        assert_eq!(EXPECTED, actual);
+    }
+
+    #[test]
+    fn string_pascal_to_snake_multiple_segments() {
+        const EXPECTED: &str = "first_second_third";
+        let input = String::from("FirstSecondThird");
+
+        let actual = input.pascal_to_snake();
+
+        assert_eq!(EXPECTED, actual);
+    }
+
+    #[test]
+    fn string_pascal_to_snake_single_segment() {
+        let input = String::from("First");
+        const EXPECTED: &str = "first";
+
+        let actual = input.pascal_to_snake();
+
+        assert_eq!(EXPECTED, actual);
+    }
+
+    #[test]
+    fn string_pascal_to_snake_empty_string() {
+        const EXPECTED: &str = "";
+        let input = String::from(EXPECTED);
+
+        let actual = input.pascal_to_snake();
+
+        assert_eq!(EXPECTED, actual);
+    }
+
+    #[test]
+    fn string_pascal_to_snake_consecutive_uppercase() {
+        const EXPECTED: &str = "kernel_mc";
+        let input = String::from("KernelMC");
+
+        let actual = input.pascal_to_snake();
+
+        assert_eq!(EXPECTED, actual);
+    }
+}

+ 90 - 0
crates/btproto/src/error.rs

@@ -0,0 +1,90 @@
+//! Code to assist with error handling, primarily [MaybeErr].
+
+/// A wrapper around `Option<syn::Error>` which allows errors to be easily combined and created
+/// from iterators of [syn::Error].
+#[derive(Debug, Default)]
+pub(crate) struct MaybeErr(Option<syn::Error>);
+
+impl MaybeErr {
+    pub(crate) fn none() -> Self {
+        MaybeErr(None)
+    }
+
+    pub(crate) fn combine(self, other: Self) -> Self {
+        let option = match (self.0, other.0) {
+            (Some(left), right) => fold_errs(right, left),
+            (left, Some(right)) => fold_errs(left, right),
+            _ => None,
+        };
+        MaybeErr(option)
+    }
+}
+
+impl From<MaybeErr> for Result<(), syn::Error> {
+    fn from(value: MaybeErr) -> Self {
+        if let Some(err) = value.0 {
+            Err(err)
+        } else {
+            Ok(())
+        }
+    }
+}
+
+impl From<syn::Error> for MaybeErr {
+    fn from(err: syn::Error) -> Self {
+        MaybeErr(Some(err))
+    }
+}
+
+impl FromIterator<syn::Error> for MaybeErr {
+    fn from_iter<T: IntoIterator<Item = syn::Error>>(iter: T) -> Self {
+        MaybeErr(iter.into_iter().fold(None, fold_errs))
+    }
+}
+
+fn fold_errs(accum: Option<syn::Error>, curr: syn::Error) -> Option<syn::Error> {
+    if let Some(mut accum) = accum {
+        accum.combine(curr);
+        Some(accum)
+    } else {
+        Some(curr)
+    }
+}
+
+#[cfg(test)]
+/// Panics if the given value does not transform into `Ok`.
+pub(crate) fn assert_ok<T, E: Into<syn::Result<T>>>(maybe_err: E) {
+    let result: syn::Result<T> = maybe_err.into();
+    assert!(result.is_ok(), "{}", result.err().unwrap());
+}
+
+#[cfg(test)]
+/// Panics if the given value does not transform into `Err` with an error value containing the
+/// given message.
+pub(crate) fn assert_err<T, E: Into<syn::Result<T>>>(maybe_err: E, expected_msg: &str) {
+    let result: syn::Result<T> = maybe_err.into();
+    assert!(result.is_err());
+    assert_eq!(expected_msg, result.err().unwrap().to_string());
+}
+
+/// User-visible compile time error messages.
+pub(crate) mod msgs {
+    /// Indicates that a duplicate transition has been defined.
+    ///
+    /// This means it is either handling the same message type in the same state sending the
+    /// same message type in the same state as another transition.
+    pub(crate) const DUPLICATE_TRANSITION: &str = "Duplicate transition.";
+    pub(crate) const NO_MSG_SENT_OR_RECEIVED: &str = "A transition must send or receive a message.";
+    pub(crate) const UNDECLARED_STATE: &str = "State was not declared.";
+    pub(crate) const UNUSED_STATE: &str = "State was declared but never used.";
+    pub(crate) const UNMATCHED_OUTGOING: &str = "No receiver found for message type.";
+    pub(crate) const UNMATCHED_INCOMING: &str = "No sender found for message type.";
+    pub(crate) const UNDELIVERABLE: &str =
+        "Receiver must either be a service, an owned state, or an out state, or the message must be a reply.";
+    pub(crate) const INVALID_REPLY: &str =
+        "Replies can only be used in transitions which handle messages.";
+    pub(crate) const MULTIPLE_REPLIES: &str =
+        "Only a single reply can be sent in response to any message.";
+    pub(crate) const UNOBSERVABLE_STATE: &str =
+        "This client state is not allowed because it only receives replies. Such a state cannot be observed.";
+}

+ 104 - 0
crates/btproto/src/generation.rs

@@ -0,0 +1,104 @@
+use proc_macro2::TokenStream;
+use quote::{format_ident, quote, ToTokens};
+
+use crate::model::{MethodModel, ProtocolModel};
+
+impl ToTokens for ProtocolModel {
+    fn to_tokens(&self, tokens: &mut TokenStream) {
+        tokens.extend(self.generate_message_enum());
+        tokens.extend(self.generate_state_traits());
+    }
+}
+
+impl ProtocolModel {
+    fn generate_message_enum(&self) -> TokenStream {
+        let msg_lookup = self.msg_lookup();
+        let get_variants = || msg_lookup.msg_iter().map(|msg| msg.msg_name());
+        let variants0 = get_variants();
+        let variants1 = get_variants();
+        let variant_names = get_variants().map(|variant| variant.to_string());
+        let msg_types = msg_lookup.msg_iter().map(|msg| msg.msg_type());
+        let all_replies = msg_lookup.msg_iter().all(|msg| msg.is_reply());
+        let enum_name = format_ident!("{}Msgs", self.def().name_def.name);
+        let send_impl = if all_replies {
+            quote! {}
+        } else {
+            quote! {
+                impl ::btrun::model::SendMsg for #enum_name {}
+            }
+        };
+        let proto_name = &self.def().name_def.name;
+        let doc_comment = format!("Message type for the {proto_name} protocol.");
+        quote! {
+            #[doc = #doc_comment]
+            #[derive(::serde::Serialize, ::serde::Deserialize)]
+            pub enum #enum_name {
+                #( #variants0(#msg_types) ),*
+            }
+
+            impl #enum_name {
+                pub fn name(&self) -> &'static str {
+                    match self {
+                        #( Self::#variants1(_) => #variant_names),*
+                    }
+                }
+            }
+
+            impl ::btrun::model::CallMsg for #enum_name {
+                type Reply = Self;
+            }
+
+            #send_impl
+        }
+    }
+
+    fn generate_state_traits(&self) -> TokenStream {
+        let traits = self.states_iter().map(|state| {
+            (
+                state.name(),
+                state.methods().values(),
+                self.actor_lookup()
+                    .actor_with_init_state(state.name())
+                    .is_some(),
+            )
+        });
+        let mut tokens = TokenStream::new();
+        for (trait_ident, methods, is_init_state) in traits {
+            let method_tokens = methods.map(|x| x.generate_tokens());
+            let actor_impl_method = if is_init_state {
+                quote! {
+                    #[doc = "The name of the implementation for the actor this state is a part of."]
+                    fn actor_impl() -> String;
+                }
+            } else {
+                quote! {}
+            };
+            quote! {
+                pub trait #trait_ident : Send + Sync + Sized {
+                    #actor_impl_method
+                    #( #method_tokens )*
+                }
+            }
+            .to_tokens(&mut tokens);
+        }
+        tokens
+    }
+}
+
+impl MethodModel {
+    /// Generates the tokens for the code which implements this transition.
+    fn generate_tokens(&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 future_name = self.future();
+        quote! {
+            #( #output_decls )*
+            type #future_name: Send + ::std::future::Future<
+                Output = ::btrun::model::TransResult<Self, ( #( #output_types ),* )>
+            >;
+            fn #method_ident(self #( , #msg_args )*) -> Self::#future_name;
+        }
+    }
+}

+ 54 - 0
crates/btproto/src/lib.rs

@@ -0,0 +1,54 @@
+extern crate proc_macro;
+use proc_macro::TokenStream;
+use quote::ToTokens;
+use syn::parse_macro_input;
+
+mod case_convert;
+mod error;
+mod generation;
+mod model;
+mod parsing;
+mod validation;
+
+use crate::{model::ProtocolModel, parsing::Protocol};
+
+macro_rules! unwrap_or_compile_err {
+    ($result:expr) => {
+        match $result {
+            Ok(value) => value,
+            Err(err) => return err.into_compile_error().into(),
+        }
+    };
+}
+
+/// Generates types for the actors participating in a messaging protocol.
+///
+/// ## Usage Restrictions
+/// You must also ensure all message types referenced by the protocol are in scope.
+///
+/// ## Grammar
+/// The grammar recognized by this macro is given below in the dialect of Extended Backus-Naur Form
+/// recognized by the `llgen` tool. The terminal symbols `Ident` and `LitInt` have the same meaning
+/// as they do in the [syn] crate.
+///
+/// ```ebnf
+/// protocol : name_def ';' ( version_def ';')? ( actor_def ';' )+ ( transition ';' )+ ;
+/// name_def : "named" Ident ;
+/// version_def: "version" LitInt;
+/// actor_def : "let" Ident '=' ident_array ;
+/// ident_array : '[' Ident ( ',' Ident )* ','? ']' ;
+/// transition : state ( '?' message )?  "->" states_list ( '>' dest_list )? ;
+/// state : Ident ident_array? ;
+/// states_list : state ( ',' state )* ','? ;
+/// dest_list : dest ( ',' dest )* ;
+/// dest : dest_state '!' message
+/// dest_state : ( "service" '(' Ident ')' ) | state ;
+/// message : Ident ( "::" "Reply" )? ident_array? ;
+/// ```
+#[proc_macro]
+pub fn protocol(input: TokenStream) -> TokenStream {
+    let input = parse_macro_input!(input as Protocol);
+    let model = unwrap_or_compile_err!(ProtocolModel::new(input));
+    unwrap_or_compile_err!(model.validate());
+    model.to_token_stream().into()
+}

+ 1028 - 0
crates/btproto/src/model.rs

@@ -0,0 +1,1028 @@
+use std::{
+    collections::{HashMap, HashSet},
+    hash::Hash,
+    rc::Rc,
+};
+
+use btrun::model::End;
+use proc_macro2::{Ident, Span, TokenStream};
+use quote::{format_ident, quote, ToTokens};
+
+use crate::{
+    case_convert::CaseConvert,
+    error,
+    parsing::MessageReplyPart,
+    parsing::{ActorDef, Dest, GetSpan, Message, Protocol, State, Transition},
+};
+
+pub(crate) struct ProtocolModel {
+    def: Protocol,
+    msg_lookup: MsgLookup,
+    actor_lookup: ActorLookup,
+    actors: HashMap<Rc<Ident>, ActorModel>,
+}
+
+impl ProtocolModel {
+    pub(crate) fn new(def: Protocol) -> syn::Result<Self> {
+        let get_transitions = || def.transitions.iter().map(|x| x.as_ref());
+        let actor_lookup =
+            ActorLookup::new(def.actor_defs.iter().map(|x| x.as_ref()), get_transitions())?;
+        let mut is_client = HashMap::<Rc<Ident>, bool>::new();
+        // First mark all actors as not clients.
+        for actor_def in def.actor_defs.iter() {
+            is_client.insert(actor_def.actor.clone(), false);
+        }
+        // For every actor which is not spawned by another actor, mark it as a client if its
+        // initial state receives no messages, then mark all of the actors it spawns as clients.
+        for actor_def in def.actor_defs.iter() {
+            if !actor_lookup.parents(actor_def.actor.as_ref()).is_empty() {
+                continue;
+            }
+            let init_state = actor_def.states.as_ref().first().unwrap();
+            let init_state_receives_no_msgs = def
+                .transitions
+                .iter()
+                .filter(|transition| {
+                    transition.in_state.state_trait.as_ref() == init_state.as_ref()
+                })
+                .all(|transition| transition.in_msg().is_none());
+            if init_state_receives_no_msgs {
+                mark_all_progeny(&actor_lookup, &mut is_client, actor_def.actor.as_ref());
+            }
+        }
+
+        fn mark_all_progeny(
+            actor_lookup: &ActorLookup,
+            is_client: &mut HashMap<Rc<Ident>, bool>,
+            actor_name: &Ident,
+        ) {
+            *is_client.get_mut(actor_name).unwrap() = true;
+            let children = actor_lookup.children(actor_name);
+            for child in children {
+                *is_client.get_mut(child).unwrap() = true;
+                mark_all_progeny(actor_lookup, is_client, child.as_ref());
+            }
+        }
+
+        let msg_lookup = MsgLookup::new(get_transitions());
+        let mut actors = HashMap::new();
+        for actor_def in def.actor_defs.iter() {
+            let actor_name = &actor_def.actor;
+            let actor_states = actor_lookup.actor_states(actor_name.as_ref());
+            let transitions_by_state = actor_states.iter().map(|state_name| {
+                let transitions = def
+                    .transitions
+                    .iter()
+                    .filter(|transition| {
+                        state_name.as_ref() == transition.in_state.state_trait.as_ref()
+                    })
+                    .cloned();
+                (state_name.clone(), transitions)
+            });
+            let actor = ActorModel::new(
+                actor_def.clone(),
+                &msg_lookup,
+                *is_client.get(&actor_def.actor).unwrap(),
+                transitions_by_state,
+            )?;
+            actors.insert(actor_name.clone(), actor);
+        }
+        Ok(Self {
+            def,
+            msg_lookup,
+            actor_lookup,
+            actors,
+        })
+    }
+
+    pub(crate) fn def(&self) -> &Protocol {
+        &self.def
+    }
+
+    pub(crate) fn msg_lookup(&self) -> &MsgLookup {
+        &self.msg_lookup
+    }
+
+    pub(crate) fn actor_lookup(&self) -> &ActorLookup {
+        &self.actor_lookup
+    }
+
+    pub(crate) fn actors_iter(&self) -> impl Iterator<Item = &ActorModel> {
+        self.actors.values()
+    }
+
+    pub(crate) fn states_iter(&self) -> impl Iterator<Item = &StateModel> {
+        self.actors_iter().flat_map(|actor| actor.states().values())
+    }
+
+    #[cfg(test)]
+    pub(crate) fn methods_iter(&self) -> impl Iterator<Item = &MethodModel> {
+        self.states_iter()
+            .flat_map(|state| state.methods().values())
+    }
+
+    #[cfg(test)]
+    pub(crate) fn outputs_iter(&self) -> impl Iterator<Item = &OutputModel> {
+        self.methods_iter()
+            .flat_map(|method| method.outputs().iter())
+    }
+}
+
+pub(crate) struct ActorModel {
+    #[allow(dead_code)]
+    def: Rc<ActorDef>,
+    /// Indicates if this actor is a client.
+    ///
+    /// A client is an actor which is not spawned by another actor (i.e. has no parents) and
+    /// whose initial state has no transition which receives a message, or which is spawned by a
+    /// client.
+    is_client: bool,
+    states: HashMap<Rc<Ident>, StateModel>,
+}
+
+impl ActorModel {
+    fn new<S, T>(
+        def: Rc<ActorDef>,
+        messages: &MsgLookup,
+        is_client: bool,
+        state_iter: S,
+    ) -> syn::Result<Self>
+    where
+        S: IntoIterator<Item = (Rc<Ident>, T)>,
+        T: IntoIterator<Item = Rc<Transition>>,
+    {
+        let transitions: HashMap<_, Vec<_>> = state_iter
+            .into_iter()
+            .map(|(name, transitions)| (name, transitions.into_iter().collect()))
+            .collect();
+        let mut states = HashMap::new();
+        for (name, transitions) in transitions.into_iter() {
+            let state = StateModel::new(name.clone(), messages, transitions, is_client)?;
+            if let Some(prev) = states.insert(name, state) {
+                panic!(
+                    "States are not being grouped by actor correctly. Duplicate state name: '{}'",
+                    prev.name
+                );
+            }
+        }
+        Ok(Self {
+            def,
+            is_client,
+            states,
+        })
+    }
+
+    pub(crate) fn is_client(&self) -> bool {
+        self.is_client
+    }
+
+    pub(crate) fn states(&self) -> &HashMap<Rc<Ident>, StateModel> {
+        &self.states
+    }
+}
+
+pub(crate) struct StateModel {
+    name: Rc<Ident>,
+    methods: HashMap<Rc<Ident>, MethodModel>,
+}
+
+impl StateModel {
+    fn new<T>(
+        name: Rc<Ident>,
+        messages: &MsgLookup,
+        transitions: T,
+        part_of_client: bool,
+    ) -> syn::Result<Self>
+    where
+        T: IntoIterator<Item = Rc<Transition>>,
+    {
+        let mut methods = HashMap::new();
+        for transition in transitions.into_iter() {
+            let transition_span = transition.span();
+            let method = MethodModel::new(transition, messages, part_of_client)?;
+            if methods.insert(method.name.clone(), method).is_some() {
+                return Err(syn::Error::new(
+                    transition_span,
+                    error::msgs::DUPLICATE_TRANSITION,
+                ));
+            }
+        }
+        Ok(Self { name, methods })
+    }
+
+    pub(crate) fn name(&self) -> &Ident {
+        self.name.as_ref()
+    }
+
+    pub(crate) fn methods(&self) -> &HashMap<Rc<Ident>, MethodModel> {
+        &self.methods
+    }
+}
+
+impl GetSpan for StateModel {
+    fn span(&self) -> Span {
+        self.name.span()
+    }
+}
+
+#[cfg_attr(test, derive(Debug))]
+pub(crate) struct MethodModel {
+    def: Rc<Transition>,
+    name: Rc<Ident>,
+    inputs: Vec<InputModel>,
+    outputs: Vec<OutputModel>,
+    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 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),
+            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())
+        } else {
+            let mut dests = def.out_msgs.as_ref().iter();
+            let mut msg_names = String::new();
+            if let Some(dest) = dests.next() {
+                msg_names.push_str(dest.msg.variant().pascal_to_snake().as_str());
+            } else {
+                return Err(syn::Error::new(
+                    def.span(),
+                    error::msgs::NO_MSG_SENT_OR_RECEIVED,
+                ));
+            }
+            for dest in dests {
+                msg_names.push('_');
+                msg_names.push_str(dest.msg.variant().pascal_to_snake().as_str());
+            }
+            format_ident!("on_send_{msg_names}")
+        };
+        Ok(name)
+    }
+
+    fn new_inputs(def: &Transition, messages: &MsgLookup, part_of_client: bool) -> Vec<InputModel> {
+        let mut inputs = Vec::new();
+        let arg_kind = if def.is_client() {
+            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,
+            ))
+        }
+        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,
+                ))
+            }
+        }
+        inputs
+    }
+
+    fn new_outputs(
+        def: &Transition,
+        type_prefix: &str,
+        messages: &MsgLookup,
+        part_of_client: bool,
+    ) -> Vec<OutputModel> {
+        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,
+            ));
+        }
+        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,
+            ))
+        }
+        outputs
+    }
+
+    pub(crate) fn def(&self) -> &Transition {
+        self.def.as_ref()
+    }
+
+    pub(crate) fn name(&self) -> &Rc<Ident> {
+        &self.name
+    }
+
+    pub(crate) fn inputs(&self) -> &Vec<InputModel> {
+        &self.inputs
+    }
+
+    pub(crate) fn outputs(&self) -> &Vec<OutputModel> {
+        &self.outputs
+    }
+
+    pub(crate) fn future(&self) -> &Ident {
+        &self.future
+    }
+}
+
+impl GetSpan for MethodModel {
+    fn span(&self) -> Span {
+        self.def.span()
+    }
+}
+
+#[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,
+    arg_type: Rc<TokenStream>,
+    arg_kind: InputKind,
+}
+
+impl InputModel {
+    fn new(type_name: Rc<Ident>, arg_type: Rc<TokenStream>, arg_kind: InputKind) -> Self {
+        let name = format_ident!("{}_arg", type_name.to_string().pascal_to_snake());
+        Self {
+            name,
+            arg_type,
+            arg_kind,
+        }
+    }
+}
+
+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 })
+    }
+}
+
+#[cfg_attr(test, derive(Debug))]
+pub(crate) struct OutputModel {
+    type_name: Option<TokenStream>,
+    decl: Option<TokenStream>,
+    #[allow(dead_code)]
+    kind: OutputKind,
+}
+
+impl OutputModel {
+    fn new(kind: OutputKind, type_prefix: &str) -> Self {
+        let (decl, type_name) = match &kind {
+            OutputKind::State { def, .. } => {
+                let state_trait = def.state_trait.as_ref();
+                if state_trait == End::ident() {
+                    let end_ident = format_ident!("{}", End::ident());
+                    (None, Some(quote! { ::btrun::model::#end_ident }))
+                } else {
+                    let type_name = format_ident!("{type_prefix}{}", state_trait);
+                    (
+                        Some(quote! { type  #type_name: #state_trait; }),
+                        Some(quote! { Self::#type_name }),
+                    )
+                }
+            }
+            OutputKind::Msg {
+                msg_type,
+                part_of_client,
+                is_call,
+                ..
+            } => {
+                let type_name = if *part_of_client {
+                    if *is_call {
+                        Some(quote! {
+                            <#msg_type as ::btrun::model::CallMsg>::Reply
+                        })
+                    } else {
+                        None
+                    }
+                } else {
+                    Some(quote! { #msg_type })
+                };
+                (None, type_name)
+            }
+        };
+        Self {
+            type_name,
+            decl,
+            kind,
+        }
+    }
+
+    pub(crate) fn type_name(&self) -> Option<&TokenStream> {
+        self.type_name.as_ref()
+    }
+
+    pub(crate) fn decl(&self) -> Option<&TokenStream> {
+        self.decl.as_ref()
+    }
+}
+
+#[cfg_attr(test, derive(Debug))]
+pub(crate) enum OutputKind {
+    State {
+        def: Rc<State>,
+    },
+    Msg {
+        #[allow(dead_code)]
+        def: Rc<Dest>,
+        msg_type: Rc<TokenStream>,
+        is_call: bool,
+        part_of_client: bool,
+    },
+}
+
+/// A type used to query information about actors, states, and their relationships.
+pub(crate) struct ActorLookup {
+    /// A map from an actor name to the set of states which are part of that actor.
+    actor_states: HashMap<Rc<Ident>, HashSet<Rc<Ident>>>,
+    #[allow(dead_code)]
+    /// A map from a state name to the actor name which that state is a part of.
+    actors_by_state: HashMap<Rc<Ident>, Rc<Ident>>,
+    /// A map from an actor name to the set of actor names which spawn it.
+    parents: HashMap<Rc<Ident>, HashSet<Rc<Ident>>>,
+    /// A map from an actor name to the set of actors names which it spawns.
+    children: HashMap<Rc<Ident>, HashSet<Rc<Ident>>>,
+    actors_by_init_state: HashMap<Rc<Ident>, Rc<Ident>>,
+}
+
+impl ActorLookup {
+    fn new<'a, A, T>(actor_defs: A, transitions: T) -> syn::Result<Self>
+    where
+        A: IntoIterator<Item = &'a ActorDef>,
+        T: IntoIterator<Item = &'a Transition>,
+    {
+        let mut actor_states = HashMap::new();
+        let mut actors_by_state = HashMap::new();
+        let mut parents = HashMap::new();
+        let mut children = HashMap::new();
+        let mut actors_by_init_state = HashMap::new();
+        for actor_def in actor_defs {
+            let mut states = HashSet::new();
+            let actor_name = &actor_def.actor;
+            let mut first = true;
+            for state in actor_def.states.as_ref().iter() {
+                if first {
+                    actors_by_init_state.insert(state.clone(), actor_name.clone());
+                    first = false;
+                }
+                states.insert(state.clone());
+                actors_by_state.insert(state.clone(), actor_def.actor.clone());
+            }
+            actor_states.insert(actor_name.clone(), states);
+            parents.insert(actor_name.clone(), HashSet::new());
+            children.insert(actor_name.clone(), HashSet::new());
+        }
+
+        for transition in transitions {
+            let in_state = transition.in_state.state_trait.as_ref();
+            let parent = actors_by_state
+                .get(in_state)
+                .ok_or_else(|| syn::Error::new(in_state.span(), error::msgs::UNDECLARED_STATE))?;
+            // The first output state is skipped because the current actor is transitioning to it,
+            // its not creating a new actor.
+            for out_state in transition.out_states.as_ref().iter().skip(1) {
+                let out_state = out_state.state_trait.as_ref();
+                let child = actors_by_state.get(out_state).ok_or_else(|| {
+                    syn::Error::new(out_state.span(), error::msgs::UNDECLARED_STATE)
+                })?;
+                parents
+                    .entry(child.clone())
+                    .or_insert_with(HashSet::new)
+                    .insert(parent.clone());
+                children
+                    .entry(parent.clone())
+                    .or_insert_with(HashSet::new)
+                    .insert(child.clone());
+            }
+        }
+
+        Ok(Self {
+            actor_states,
+            actors_by_state,
+            parents,
+            children,
+            actors_by_init_state,
+        })
+    }
+
+    const UNKNOWN_ACTOR_ERR: &str =
+        "Unknown actor. This indicates there is a bug in the btproto crate.";
+
+    /// Returns the set of states associated with the given actor.
+    ///
+    /// This method **panics** if you call it with a non-existent actor name.
+    pub(crate) fn actor_states(&self, actor_name: &Ident) -> &HashSet<Rc<Ident>> {
+        self.actor_states
+            .get(actor_name)
+            .unwrap_or_else(|| panic!("actor_states: {}", Self::UNKNOWN_ACTOR_ERR))
+    }
+
+    pub(crate) fn parents(&self, actor_name: &Ident) -> &HashSet<Rc<Ident>> {
+        self.parents
+            .get(actor_name)
+            .unwrap_or_else(|| panic!("parents: {}", Self::UNKNOWN_ACTOR_ERR))
+    }
+
+    pub(crate) fn children(&self, actor_name: &Ident) -> &HashSet<Rc<Ident>> {
+        self.children
+            .get(actor_name)
+            .unwrap_or_else(|| panic!("children: {}", Self::UNKNOWN_ACTOR_ERR))
+    }
+
+    /// Returns the name of the actor for which the given state name is the initial state. If the
+    /// given name is not the initial state of any actor, [None] is returned.
+    pub(crate) fn actor_with_init_state(&self, init_state: &Ident) -> Option<&Rc<Ident>> {
+        self.actors_by_init_state.get(init_state)
+    }
+}
+
+pub(crate) struct MsgLookup {
+    messages: HashMap<Rc<Ident>, MsgInfo>,
+}
+
+impl MsgLookup {
+    fn new<'a>(transitions: impl IntoIterator<Item = &'a Transition>) -> Self {
+        let mut messages = HashMap::new();
+        for transition in transitions.into_iter() {
+            let in_state = &transition.in_state.state_trait;
+            if let Some(in_msg) = transition.in_msg() {
+                let msg_name = &in_msg.msg_type;
+                let msg_info = messages
+                    .entry(msg_name.clone())
+                    .or_insert_with(|| MsgInfo::empty(in_msg.clone()));
+                msg_info.record_receiver(in_msg, in_state.clone());
+            }
+            for dest in transition.out_msgs.as_ref().iter() {
+                let msg = &dest.msg;
+                let msg_info = messages
+                    .entry(msg.msg_type.clone())
+                    .or_insert_with(|| MsgInfo::empty(msg.clone()));
+                msg_info.record_sender(&dest.msg, in_state.clone());
+            }
+        }
+        Self { messages }
+    }
+
+    pub(crate) fn lookup(&self, msg: &Message) -> &MsgInfo {
+        self.messages
+            .get(msg.msg_type.as_ref())
+            // Since a message is either sent or received, and we've added all such messages in
+            // the new method, this unwrap should not panic.
+            .unwrap_or_else(|| {
+                panic!("Failed to find message info. There is a bug in MessageLookup::new.")
+            })
+            .info_for(msg)
+    }
+
+    pub(crate) fn msg_iter(&self) -> impl Iterator<Item = &MsgInfo> {
+        self.messages
+            .values()
+            .flat_map(|msg_info| [Some(msg_info), msg_info.reply()])
+            .flatten()
+    }
+}
+
+impl AsRef<HashMap<Rc<Ident>, MsgInfo>> for MsgLookup {
+    fn as_ref(&self) -> &HashMap<Rc<Ident>, MsgInfo> {
+        &self.messages
+    }
+}
+
+#[cfg_attr(test, derive(Debug))]
+pub(crate) struct MsgInfo {
+    def: Rc<Message>,
+    msg_name: Rc<Ident>,
+    msg_type: Rc<TokenStream>,
+    is_reply: bool,
+    senders: HashSet<Rc<Ident>>,
+    receivers: HashSet<Rc<Ident>>,
+    reply: Option<Box<MsgInfo>>,
+}
+
+impl MsgInfo {
+    fn empty(def: Rc<Message>) -> Self {
+        let msg_name = def.msg_type.as_ref();
+        Self {
+            msg_name: def.msg_type.clone(),
+            msg_type: Rc::new(quote! { #msg_name }),
+            is_reply: false,
+            senders: HashSet::new(),
+            receivers: HashSet::new(),
+            reply: None,
+            def,
+        }
+    }
+
+    fn info_for(&self, msg: &Message) -> &Self {
+        if msg.is_reply() {
+            // If this message is a reply, then we should have seen it in MsgLookup::new and
+            // initialized its reply pointer. So this unwrap shouldn't panic.
+            self.reply.as_ref().unwrap_or_else(|| {
+                panic!(
+                "A reply message was not properly recorded. There is a bug in MessageLookup::new."
+            )
+            })
+        } else {
+            self
+        }
+    }
+
+    fn info_for_mut(&mut self, msg: &Rc<Message>) -> &mut Self {
+        if msg.is_reply() {
+            self.reply.get_or_insert_with(|| {
+                let mut reply = MsgInfo::empty(msg.clone());
+                reply.is_reply = true;
+                reply.msg_name = Rc::new(format_ident!(
+                    "{}{}",
+                    msg.msg_type.as_ref(),
+                    MessageReplyPart::REPLY_IDENT
+                ));
+                let parent = self.msg_name.as_ref();
+                reply.msg_type = Rc::new(quote! { <#parent as ::btrun::model::CallMsg>::Reply });
+                Box::new(reply)
+            })
+        } else {
+            self
+        }
+    }
+
+    fn record_receiver(&mut self, msg: &Rc<Message>, receiver: Rc<Ident>) {
+        let target = self.info_for_mut(msg);
+        target.receivers.insert(receiver);
+    }
+
+    fn record_sender(&mut self, msg: &Rc<Message>, sender: Rc<Ident>) {
+        let target = self.info_for_mut(msg);
+        target.senders.insert(sender);
+    }
+
+    pub(crate) fn def(&self) -> &Rc<Message> {
+        &self.def
+    }
+
+    /// The unique name of this message. If it is a reply, it will end in
+    /// `MessageReplyPart::REPLY_IDENT`.
+    pub(crate) fn msg_name(&self) -> &Rc<Ident> {
+        &self.msg_name
+    }
+
+    /// The type of this message. If this message is not a reply, this is just `msg_name`. If it is
+    /// a reply, it is `<#msg_name as ::btrun::model::CallMsg>::Reply`.
+    pub(crate) fn msg_type(&self) -> &Rc<TokenStream> {
+        &self.msg_type
+    }
+
+    pub(crate) fn reply(&self) -> Option<&MsgInfo> {
+        self.reply.as_ref().map(|ptr| ptr.as_ref())
+    }
+
+    pub(crate) fn is_reply(&self) -> bool {
+        self.is_reply
+    }
+
+    /// Returns true iff this message is a call.
+    pub(crate) fn is_call(&self) -> bool {
+        self.reply.is_some()
+    }
+}
+
+impl PartialEq for MsgInfo {
+    fn eq(&self, other: &Self) -> bool {
+        self.msg_name.as_ref() == other.msg_name.as_ref()
+    }
+}
+
+impl Eq for MsgInfo {}
+
+impl Hash for MsgInfo {
+    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+        self.msg_name.hash(state)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::{
+        error::{assert_err, assert_ok},
+        parsing::{DestinationState, NameDef},
+    };
+
+    use super::*;
+
+    #[test]
+    fn protocol_model_new_minimal_ok() {
+        let input = Protocol::minimal();
+
+        let result = ProtocolModel::new(input);
+
+        assert_ok(result);
+    }
+
+    #[test]
+    fn protocol_model_new_dup_recv_transition_err() {
+        let input = Protocol::new(
+            NameDef::new("Undeclared"),
+            [ActorDef::new("actor", ["Init", "Next"])],
+            [
+                Transition::new(
+                    State::new("Init", []),
+                    Some(Message::new("Query", false, [])),
+                    [State::new("Next", [])],
+                    [],
+                ),
+                Transition::new(
+                    State::new("Init", []),
+                    Some(Message::new("Query", false, [])),
+                    [State::new("Init", [])],
+                    [],
+                ),
+            ],
+        );
+
+        let result = ProtocolModel::new(input);
+
+        assert_err(result, error::msgs::DUPLICATE_TRANSITION);
+    }
+
+    #[test]
+    fn protocol_model_new_dup_send_transition_err() {
+        let input = Protocol::new(
+            NameDef::new("Undeclared"),
+            [
+                ActorDef::new("server", ["Init", "Next"]),
+                ActorDef::new("client", ["Client"]),
+            ],
+            [
+                Transition::new(
+                    State::new("Client", []),
+                    None,
+                    [State::new("Client", [])],
+                    [Dest::new(
+                        DestinationState::Individual(State::new("Init", [])),
+                        Message::new("Query", false, []),
+                    )],
+                ),
+                Transition::new(
+                    State::new("Client", []),
+                    None,
+                    [State::new("Client", [])],
+                    [Dest::new(
+                        DestinationState::Individual(State::new("Next", [])),
+                        Message::new("Query", false, []),
+                    )],
+                ),
+            ],
+        );
+
+        let result = ProtocolModel::new(input);
+
+        assert_err(result, error::msgs::DUPLICATE_TRANSITION);
+    }
+
+    #[test]
+    fn msg_sent_or_received_msg_received_ok() {
+        let input = Protocol::new(
+            NameDef::new("Test"),
+            [ActorDef::new("actor", ["Init"])],
+            [Transition::new(
+                State::new("Init", []),
+                Some(Message::new("Activate", false, [])),
+                [State::new("End", [])],
+                [],
+            )],
+        );
+
+        let result = ProtocolModel::new(input);
+
+        assert_ok(result);
+    }
+
+    #[test]
+    fn msg_sent_or_received_msg_sent_ok() {
+        let input = Protocol::new(
+            NameDef::new("Test"),
+            [ActorDef::new("actor", ["First", "Second"])],
+            [Transition::new(
+                State::new("First", []),
+                None,
+                [State::new("First", [])],
+                [Dest::new(
+                    DestinationState::Individual(State::new("Second", [])),
+                    Message::new("Msg", false, []),
+                )],
+            )],
+        );
+
+        let result = ProtocolModel::new(input);
+
+        assert_ok(result);
+    }
+
+    #[test]
+    fn msg_sent_or_received_neither_err() {
+        let input = Protocol::new(
+            NameDef::new("Test"),
+            [ActorDef::new("actor", ["First"])],
+            [Transition::new(
+                State::new("First", []),
+                None,
+                [State::new("First", [])],
+                [],
+            )],
+        );
+
+        let result = ProtocolModel::new(input);
+
+        assert_err(result, error::msgs::NO_MSG_SENT_OR_RECEIVED);
+    }
+
+    #[test]
+    fn reply_is_marked_in_output() {
+        const MSG: &str = "Ping";
+        let input = Protocol::new(
+            NameDef::new("ReplyTest"),
+            [
+                ActorDef::new("server", ["Listening"]),
+                ActorDef::new("client", ["Client", "Waiting"]),
+            ],
+            [
+                Transition::new(
+                    State::new("Client", []),
+                    None,
+                    [State::new("Waiting", [])],
+                    [Dest::new(
+                        DestinationState::Service(State::new("Listening", [])),
+                        Message::new(MSG, false, []),
+                    )],
+                ),
+                Transition::new(
+                    State::new("Listening", []),
+                    Some(Message::new(MSG, false, [])),
+                    [State::new("Listening", [])],
+                    [Dest::new(
+                        DestinationState::Individual(State::new("Client", [])),
+                        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());
+    }
+
+    fn simple_client_server_proto() -> Protocol {
+        Protocol::new(
+            NameDef::new("IsClientTest"),
+            [
+                ActorDef::new("server", ["Server"]),
+                ActorDef::new("client", ["Client"]),
+            ],
+            [
+                Transition::new(
+                    State::new("Client", []),
+                    None,
+                    [State::new("End", [])],
+                    [Dest::new(
+                        DestinationState::Service(State::new("Server", [])),
+                        Message::new("Msg", false, []),
+                    )],
+                ),
+                Transition::new(
+                    State::new("Server", []),
+                    Some(Message::new("Msg", false, [])),
+                    [State::new("End", [])],
+                    [],
+                ),
+            ],
+        )
+    }
+
+    #[test]
+    fn is_client_false_for_server() {
+        let input = simple_client_server_proto();
+
+        let actual = ProtocolModel::new(input).unwrap();
+
+        let server = actual.actors.get(&format_ident!("server")).unwrap();
+        assert!(!server.is_client());
+    }
+
+    #[test]
+    fn is_client_true_for_client() {
+        let input = simple_client_server_proto();
+
+        let actual = ProtocolModel::new(input).unwrap();
+
+        let client = actual.actors.get(&format_ident!("client")).unwrap();
+        assert!(client.is_client());
+    }
+
+    #[test]
+    fn is_client_false_for_worker() {
+        let input = Protocol::new(
+            NameDef::new("IsClientTest"),
+            [
+                ActorDef::new("server", ["Listening"]),
+                ActorDef::new("worker", ["Working"]),
+                ActorDef::new("client", ["Unregistered", "Registered"]),
+            ],
+            [
+                Transition::new(
+                    State::new("Unregistered", []),
+                    None,
+                    [State::new("Registered", [])],
+                    [Dest::new(
+                        DestinationState::Service(State::new("Listening", [])),
+                        Message::new("Register", false, ["Registered"]),
+                    )],
+                ),
+                Transition::new(
+                    State::new("Listening", []),
+                    Some(Message::new("Register", false, ["Registered"])),
+                    [
+                        State::new("Listening", []),
+                        State::new("Working", ["Registered"]),
+                    ],
+                    [],
+                ),
+                Transition::new(
+                    State::new("Working", ["Registered"]),
+                    None,
+                    [State::new("End", [])],
+                    [Dest::new(
+                        DestinationState::Individual(State::new("Registered", [])),
+                        Message::new("Completed", false, []),
+                    )],
+                ),
+                Transition::new(
+                    State::new("Registered", []),
+                    Some(Message::new("Completed", false, [])),
+                    [State::new("End", [])],
+                    [],
+                ),
+            ],
+        );
+
+        let actual = ProtocolModel::new(input).unwrap();
+
+        let worker = actual.actors.get(&format_ident!("worker")).unwrap();
+        assert!(!worker.is_client());
+    }
+}

+ 1382 - 0
crates/btproto/src/parsing.rs

@@ -0,0 +1,1382 @@
+//! Types for parsing the protocol grammar.
+
+use proc_macro2::Span;
+use quote::format_ident;
+use std::{ops::Deref, rc::Rc};
+use syn::{
+    bracketed, parenthesized,
+    parse::{Parse, ParseStream},
+    punctuated::Punctuated,
+    spanned::Spanned,
+    token::Bracket,
+    Ident, LitInt, Token,
+};
+
+/// This type represents the top-level production for the protocol grammar.
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct Protocol {
+    pub(crate) name_def: NameDef,
+    name_def_semi: Token![;],
+    #[allow(dead_code)]
+    version_def: Option<(VersionDef, Token![;])>,
+    pub(crate) actor_defs: Punctuated<Rc<ActorDef>, Token![;]>,
+    pub(crate) transitions: Punctuated<Rc<Transition>, Token![;]>,
+}
+
+#[cfg(test)]
+impl Protocol {
+    pub(crate) fn new(
+        name_def: NameDef,
+        actor_def: impl IntoIterator<Item = ActorDef>,
+        transitions: impl IntoIterator<Item = Transition>,
+    ) -> Self {
+        let mut actor_def: Punctuated<Rc<ActorDef>, Token![;]> =
+            actor_def.into_iter().map(Rc::new).collect();
+        actor_def.push_punct(Token![;](Span::call_site()));
+        let mut transitions: Punctuated<Rc<Transition>, Token![;]> =
+            transitions.into_iter().map(Rc::new).collect();
+        transitions.push_punct(Token![;](Span::call_site()));
+        Self {
+            name_def,
+            name_def_semi: Token![;](Span::call_site()),
+            version_def: None,
+            actor_defs: actor_def,
+            transitions,
+        }
+    }
+
+    pub(crate) fn with_version(
+        name_def: NameDef,
+        version_def: VersionDef,
+        actor_def: impl IntoIterator<Item = ActorDef>,
+        transitions: impl IntoIterator<Item = Transition>,
+    ) -> Self {
+        let mut protocol = Self::new(name_def, actor_def, transitions);
+        protocol.version_def = Some((version_def, Token![;](Span::call_site())));
+        protocol
+    }
+
+    /// Creates minimal [Protocol] value.
+    pub(crate) fn minimal() -> Protocol {
+        const STATE_NAME: &str = "Init";
+        Protocol::new(
+            NameDef::new("Test"),
+            [
+                ActorDef::new("server", [STATE_NAME]),
+                ActorDef::new("client", ["Client"]),
+            ],
+            [
+                Transition::new(
+                    State::new("Client", []),
+                    None,
+                    [State::new("End", [])],
+                    [Dest::new(
+                        DestinationState::Service(State::new(STATE_NAME, [])),
+                        Message::new("Msg", false, []),
+                    )],
+                ),
+                Transition::new(
+                    State::new(STATE_NAME, []),
+                    Some(Message::new("Msg", false, [])),
+                    [State::new("End", [])],
+                    [],
+                ),
+            ],
+        )
+    }
+}
+
+impl Parse for Protocol {
+    /// protocol : name_def ';' ( version_def ';')? ( actor_def ';' )+ ( transition ';' )+ ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let name_def = input.parse()?;
+        let name_def_semi = input.parse()?;
+        let version_def = if let Some((ident, _)) = input.cursor().ident() {
+            if ident == VersionDef::VERSION_IDENT {
+                Some((input.parse::<VersionDef>()?, input.parse::<Token![;]>()?))
+            } else {
+                None
+            }
+        } else {
+            None
+        };
+        let actor_defs = Punctuated::parse_list(input, |input| !input.peek(Token![let]))?;
+        let transitions =
+            input.parse_terminated(|input| Ok(Rc::new(Transition::parse(input)?)), Token![;])?;
+        Ok(Protocol {
+            name_def,
+            name_def_semi,
+            version_def,
+            actor_defs,
+            transitions,
+        })
+    }
+}
+
+impl GetSpan for Protocol {
+    fn span(&self) -> Span {
+        self.name_def
+            .span()
+            .left_join(
+                self.version_def
+                    .as_ref()
+                    .map(|(left, right)| left.span().left_join(right.span())),
+            )
+            .left_join(self.name_def_semi.span())
+            .left_join(punctuated_span(&self.actor_defs))
+            .left_join(punctuated_span(&self.transitions))
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct NameDef {
+    named_ident: Ident,
+    pub(crate) name: Ident,
+}
+
+impl NameDef {
+    const NAME_IDENT: &str = "named";
+    const NAME_IDENT_ERR: &str = "Invalid name declaration. Expected 'named'.";
+}
+
+#[cfg(test)]
+impl NameDef {
+    pub(crate) fn new(name: &str) -> Self {
+        Self {
+            named_ident: new_ident(Self::NAME_IDENT),
+            name: new_ident(name),
+        }
+    }
+}
+
+impl Parse for NameDef {
+    /// name_def : "named" Ident ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        Ok(NameDef {
+            named_ident: check_ident(input.parse()?, Self::NAME_IDENT, Self::NAME_IDENT_ERR)?,
+            name: input.parse()?,
+        })
+    }
+}
+
+impl GetSpan for NameDef {
+    fn span(&self) -> Span {
+        self.named_ident.span().left_join(self.name.span())
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct VersionDef {
+    version_ident: Ident,
+    literal: LitInt,
+    version: u64,
+}
+
+impl VersionDef {
+    const VERSION_IDENT: &str = "version";
+    const VERSION_IDENT_ERR: &str = "Invalid version specifier. Expected 'version'.";
+
+    #[cfg(test)]
+    pub(crate) fn new(version: u64) -> Self {
+        Self {
+            version_ident: new_ident("version"),
+            literal: LitInt::new(&version.to_string(), Span::call_site()),
+            version,
+        }
+    }
+
+    #[allow(dead_code)]
+    pub(crate) fn version(&self) -> u64 {
+        self.version
+    }
+}
+
+impl Parse for VersionDef {
+    /// version_def: "version" LitInt;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let version_ident =
+            check_ident(input.parse()?, Self::VERSION_IDENT, Self::VERSION_IDENT_ERR)?;
+        let literal: LitInt = input.parse()?;
+        let version = literal.base10_parse()?;
+        Ok(Self {
+            version_ident,
+            literal,
+            version,
+        })
+    }
+}
+
+impl GetSpan for VersionDef {
+    fn span(&self) -> Span {
+        self.version_ident.span().left_join(self.literal.span())
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct ActorDef {
+    let_token: Token![let],
+    pub(crate) actor: Rc<Ident>,
+    eq_token: Token![=],
+    pub(crate) states: IdentArray,
+}
+
+#[cfg(test)]
+impl ActorDef {
+    pub(crate) fn new<T, I>(actor: &str, state_names: T) -> Self
+    where
+        T: IntoIterator<IntoIter = I>,
+        I: ExactSizeIterator<Item = &'static str>,
+    {
+        Self {
+            let_token: Token![let](Span::call_site()),
+            actor: new_ident(actor).into(),
+            eq_token: Token![=](Span::call_site()),
+            states: IdentArray::new(state_names).unwrap(),
+        }
+    }
+}
+
+impl Parse for ActorDef {
+    /// actor_def : "let" Ident '=' ident_array ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        Ok(Self {
+            let_token: input.parse()?,
+            actor: Rc::new(input.parse()?),
+            eq_token: input.parse()?,
+            states: input.parse()?,
+        })
+    }
+}
+
+impl GetSpan for ActorDef {
+    fn span(&self) -> Span {
+        self.let_token
+            .span()
+            .left_join(self.actor.span())
+            .left_join(self.eq_token.span())
+            .left_join(self.states.span())
+    }
+}
+
+#[cfg_attr(test, derive(Debug))]
+#[derive(Hash, PartialEq, Eq)]
+pub(crate) struct IdentArray {
+    bracket: Bracket,
+    idents: Punctuated<Rc<Ident>, Token![,]>,
+}
+
+impl IdentArray {
+    const EMPTY_ERR: &str = "at least one state is required";
+}
+
+#[cfg(test)]
+impl IdentArray {
+    pub(crate) fn new<T, I>(state_names: T) -> Option<Self>
+    where
+        T: IntoIterator<IntoIter = I>,
+        I: ExactSizeIterator<Item = &'static str>,
+    {
+        let state_names = state_names.into_iter();
+        if state_names.len() > 0 {
+            Some(Self {
+                bracket: Bracket::default(),
+                idents: state_names.map(new_ident).map(Rc::new).collect(),
+            })
+        } else {
+            None
+        }
+    }
+}
+
+impl GetSpan for IdentArray {
+    fn span(&self) -> Span {
+        let delim_span = &self.bracket.span;
+        delim_span
+            .open()
+            .left_join(self.idents.span())
+            .left_join(delim_span.close())
+    }
+}
+
+impl Parse for IdentArray {
+    /// ident_array : '[' Ident ( ',' Ident )* ','? ']' ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let content;
+        let bracket = bracketed!(content in input);
+        let idents =
+            content.parse_terminated(|input| Ok(Rc::new(Ident::parse(input)?)), Token![,])?;
+        if idents.is_empty() {
+            return Err(syn::Error::new(bracket.span.open(), Self::EMPTY_ERR));
+        }
+        Ok(Self { bracket, idents })
+    }
+}
+
+impl AsRef<Punctuated<Rc<Ident>, Token![,]>> for IdentArray {
+    fn as_ref(&self) -> &Punctuated<Rc<Ident>, Token![,]> {
+        &self.idents
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct Transition {
+    pub(crate) in_state: State,
+    in_msg: Option<(Token![?], Rc<Message>)>,
+    arrow: Token![->],
+    pub(crate) out_states: StatesList,
+    #[allow(dead_code)]
+    redirect: Option<Token![>]>,
+    pub(crate) out_msgs: DestList,
+}
+
+impl Transition {
+    pub(crate) fn in_msg(&self) -> Option<&Rc<Message>> {
+        self.in_msg.as_ref().map(|(_, msg)| msg)
+    }
+
+    pub(crate) fn is_client(&self) -> bool {
+        self.in_msg.is_none()
+    }
+}
+
+#[cfg(test)]
+impl Transition {
+    pub(crate) fn new(
+        in_state: State,
+        in_msg: Option<Message>,
+        out_states: impl IntoIterator<Item = State>,
+        out_msgs: impl IntoIterator<Item = Dest>,
+    ) -> Self {
+        let out_msgs = DestList(out_msgs.into_iter().map(Rc::new).collect());
+        let redirect = if out_msgs.as_ref().is_empty() {
+            None
+        } else {
+            Some(Token![>](Span::call_site()))
+        };
+        Self {
+            in_state,
+            in_msg: in_msg.map(|msg| (Token![?](Span::call_site()), Rc::new(msg))),
+            arrow: Token![->](Span::call_site()),
+            out_states: StatesList(out_states.into_iter().map(Rc::new).collect()),
+            redirect,
+            out_msgs,
+        }
+    }
+}
+
+impl Parse for Transition {
+    /// transition : state ( '?' message )?  "->" states_list ( '>' dest_list )? ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let in_state = State::parse(input)?;
+        let in_msg = if let Ok(question_mark) = input.parse::<Token![?]>() {
+            Some((question_mark, Rc::new(Message::parse(input)?)))
+        } else {
+            None
+        };
+        let arrow = input.parse::<Token![->]>()?;
+        let out_states = StatesList::parse(input)?;
+        let (redirect, out_msgs) = if let Ok(redirect) = input.parse::<Token![>]>() {
+            (Some(redirect), DestList::parse(input)?)
+        } else {
+            (None, DestList::empty())
+        };
+        Ok(Self {
+            in_state,
+            in_msg,
+            arrow,
+            out_states,
+            redirect,
+            out_msgs,
+        })
+    }
+}
+
+impl GetSpan for Transition {
+    fn span(&self) -> Span {
+        self.arrow.span()
+    }
+}
+
+struct IdentIter<'a> {
+    idents: Option<syn::punctuated::Iter<'a, Rc<Ident>>>,
+}
+
+impl<'a> IdentIter<'a> {
+    fn new(idents: Option<syn::punctuated::Iter<'a, Rc<Ident>>>) -> Self {
+        Self { idents }
+    }
+}
+
+impl<'a> Iterator for IdentIter<'a> {
+    type Item = &'a Rc<Ident>;
+    fn next(&mut self) -> Option<Self::Item> {
+        if let Some(idents) = self.idents.as_mut() {
+            idents.next()
+        } else {
+            None
+        }
+    }
+}
+
+#[cfg_attr(test, derive(Debug))]
+#[derive(Hash, PartialEq, Eq)]
+pub(crate) struct State {
+    pub(crate) state_trait: Rc<Ident>,
+    pub(crate) owned_states: Option<IdentArray>,
+}
+
+impl State {
+    pub(crate) fn owned_states(&self) -> impl Iterator<Item = &'_ Rc<Ident>> {
+        IdentIter::new(
+            self.owned_states
+                .as_ref()
+                .map(|idents| idents.as_ref().iter()),
+        )
+    }
+}
+
+#[cfg(test)]
+impl State {
+    pub(crate) fn new<T, I>(state_trait: &str, owned_states: T) -> Self
+    where
+        T: IntoIterator<IntoIter = I>,
+        I: ExactSizeIterator<Item = &'static str>,
+    {
+        Self {
+            state_trait: new_ident(state_trait).into(),
+            owned_states: IdentArray::new(owned_states),
+        }
+    }
+
+    fn new_empty_owned(state_trait: &str) -> Self {
+        Self::new(state_trait, std::iter::empty())
+    }
+}
+
+impl Parse for State {
+    /// state : Ident ident_array? ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let state_trait = Ident::parse(input)?.into();
+        let owned_states = if input.peek(Bracket) {
+            Some(IdentArray::parse(input)?)
+        } else {
+            None
+        };
+        Ok(Self {
+            state_trait,
+            owned_states,
+        })
+    }
+}
+
+impl GetSpan for State {
+    fn span(&self) -> Span {
+        self.state_trait.span().left_join(&self.owned_states)
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct StatesList(Punctuated<Rc<State>, Token![,]>);
+
+impl StatesList {
+    const EMPTY_ERR: &str = "at lest one out state is required";
+}
+
+impl Parse for StatesList {
+    /// states_list : state ( ',' state )* ','? ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let states = Punctuated::parse_list(input, |input| {
+            input.peek(Token![>]) || input.peek(Token![;])
+        })?;
+        if states.is_empty() {
+            return Err(input.error(Self::EMPTY_ERR));
+        }
+        Ok(Self(states))
+    }
+}
+
+impl GetSpan for StatesList {
+    fn span(&self) -> Span {
+        punctuated_span(&self.0)
+    }
+}
+
+impl AsRef<Punctuated<Rc<State>, Token![,]>> for StatesList {
+    fn as_ref(&self) -> &Punctuated<Rc<State>, Token![,]> {
+        &self.0
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct DestList(Punctuated<Rc<Dest>, Token![,]>);
+
+impl DestList {
+    const TRAILING_COMMA_ERR: &str = "No trailing comma is allowed in a destination list.";
+
+    fn empty() -> Self {
+        Self(Punctuated::new())
+    }
+}
+
+impl Parse for DestList {
+    /// dest_list : dest ( ',' dest )* ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let dests = Punctuated::parse_list(input, |input| input.peek(Token![;]))?;
+        if dests.trailing_punct() {
+            return Err(syn::Error::new(
+                punctuated_span(&dests),
+                Self::TRAILING_COMMA_ERR,
+            ));
+        }
+        Ok(DestList(dests))
+    }
+}
+
+impl MaybeGetSpan for DestList {
+    fn maybe_span(&self) -> Option<Span> {
+        if self.0.is_empty() {
+            None
+        } else {
+            Some(punctuated_span(&self.0))
+        }
+    }
+}
+
+impl AsRef<Punctuated<Rc<Dest>, Token![,]>> for DestList {
+    fn as_ref(&self) -> &Punctuated<Rc<Dest>, Token![,]> {
+        &self.0
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct Dest {
+    pub(crate) state: DestinationState,
+    exclamation: Token![!],
+    pub(crate) msg: Rc<Message>,
+}
+
+#[cfg(test)]
+impl Dest {
+    pub(crate) fn new(state: DestinationState, msg: Message) -> Self {
+        Self {
+            state,
+            exclamation: Token![!](Span::call_site()),
+            msg: Rc::new(msg),
+        }
+    }
+}
+
+impl Parse for Dest {
+    /// dest : dest_state '!' message
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        Ok(Self {
+            state: input.parse()?,
+            exclamation: input.parse()?,
+            msg: Rc::new(input.parse()?),
+        })
+    }
+}
+
+impl GetSpan for Dest {
+    fn span(&self) -> Span {
+        self.state
+            .span()
+            .left_join(self.exclamation.span())
+            .left_join(self.msg.span())
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) enum DestinationState {
+    Service(State),
+    Individual(State),
+}
+
+impl DestinationState {
+    const NO_STATE_ERR: &str = "expected destination state";
+    const MULTI_STATE_ERR: &str = "only one destination state is allowed";
+}
+
+impl Parse for DestinationState {
+    /// dest_state : ( "service" '(' Ident ')' ) | state ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let tester = input.fork();
+        let ident = Ident::parse(&tester)?;
+        if ident == "service" {
+            Ident::parse(input)?;
+            let content;
+            let paren_token = parenthesized!(content in input);
+            let mut dest_states = content
+                .parse_terminated(Ident::parse, Token![,])?
+                .into_iter();
+            let dest_state = dest_states
+                .next()
+                .ok_or(syn::Error::new(paren_token.span.open(), Self::NO_STATE_ERR))?;
+            if let Some(extra_dest) = dest_states.next() {
+                return Err(syn::Error::new(extra_dest.span(), Self::MULTI_STATE_ERR));
+            }
+            Ok(DestinationState::Service(State {
+                state_trait: dest_state.into(),
+                owned_states: None,
+            }))
+        } else {
+            Ok(DestinationState::Individual(input.parse()?))
+        }
+    }
+}
+
+impl GetSpan for DestinationState {
+    fn span(&self) -> Span {
+        let state = match self {
+            Self::Service(state) => state,
+            Self::Individual(state) => state,
+        };
+        state.span()
+    }
+}
+
+#[cfg_attr(test, derive(Debug))]
+#[derive(Hash, PartialEq, Eq)]
+pub(crate) struct Message {
+    pub(crate) msg_type: Rc<Ident>,
+    reply_part: Option<MessageReplyPart>,
+    pub(crate) owned_states: Option<IdentArray>,
+    ident: Option<Ident>,
+}
+
+impl Message {
+    /// Returns the identifier to use when naming types and variants after this message.
+    pub(crate) fn variant(&self) -> &Ident {
+        if let Some(ident) = &self.ident {
+            ident
+        } else {
+            &self.msg_type
+        }
+    }
+
+    /// Returns true if and only if this message is a reply.
+    pub(crate) fn is_reply(&self) -> bool {
+        self.reply_part.is_some()
+    }
+
+    pub(crate) fn owned_states(&self) -> impl Iterator<Item = &'_ Rc<Ident>> {
+        IdentIter::new(
+            self.owned_states
+                .as_ref()
+                .map(|states| states.as_ref().iter()),
+        )
+    }
+}
+
+#[cfg(test)]
+impl Message {
+    pub(crate) fn new<T, I>(msg_type: &str, is_reply: bool, owned_states: T) -> Self
+    where
+        T: IntoIterator<IntoIter = I>,
+        I: ExactSizeIterator<Item = &'static str>,
+    {
+        let (reply_part, ident_field) = if is_reply {
+            let reply_part = MessageReplyPart {
+                colons: Token![::](Span::call_site()),
+                reply: new_ident(MessageReplyPart::REPLY_IDENT),
+            };
+            let variant = format_ident!("{}{}", msg_type, MessageReplyPart::REPLY_IDENT);
+            (Some(reply_part), Some(variant))
+        } else {
+            (None, None)
+        };
+        Self {
+            msg_type: Rc::new(new_ident(msg_type)),
+            reply_part,
+            owned_states: IdentArray::new(owned_states),
+            ident: ident_field,
+        }
+    }
+}
+
+impl Parse for Message {
+    /// message : Ident ( "::" "Reply" )? ident_array? ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let msg_type = Rc::new(Ident::parse(input)?);
+        let (reply_part, ident) = if input.peek(Token![::]) {
+            let reply_part = input.parse()?;
+            (
+                Some(reply_part),
+                Some(format_ident!("{msg_type}{}", MessageReplyPart::REPLY_IDENT)),
+            )
+        } else {
+            (None, None)
+        };
+        let owned_states = if input.peek(Bracket) {
+            Some(IdentArray::parse(input)?)
+        } else {
+            None
+        };
+        Ok(Self {
+            msg_type,
+            reply_part,
+            owned_states,
+            ident,
+        })
+    }
+}
+
+impl GetSpan for Message {
+    fn span(&self) -> Span {
+        self.msg_type
+            .span()
+            .left_join(self.reply_part.as_ref())
+            .left_join(&self.owned_states)
+    }
+}
+
+#[cfg_attr(test, derive(Debug))]
+#[derive(Hash, PartialEq, Eq)]
+pub(crate) struct MessageReplyPart {
+    colons: Token![::],
+    reply: Ident,
+}
+
+impl MessageReplyPart {
+    const REPLY_ERR: &str = "expected 'Reply'";
+    pub(crate) const REPLY_IDENT: &str = "Reply";
+}
+
+impl Parse for MessageReplyPart {
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        Ok(Self {
+            colons: input.parse()?,
+            reply: check_ident(input.parse()?, Self::REPLY_IDENT, Self::REPLY_ERR)?,
+        })
+    }
+}
+
+impl GetSpan for MessageReplyPart {
+    fn span(&self) -> Span {
+        self.colons.span().left_join(self.reply.span())
+    }
+}
+
+/// Verifies that an ident has the expected string contents and returns it if it does. An error is
+/// returned containing the given error message if it does not.
+fn check_ident(ident: Ident, expected: &str, err_msg: &str) -> syn::Result<Ident> {
+    if ident == expected {
+        Ok(ident)
+    } else {
+        Err(syn::Error::new(ident.span(), err_msg))
+    }
+}
+
+/// Trait for types which represent a region of source code. This is similar to the [Spanned] trait
+/// in the [syn] crate. A new trait was needed because [Spanned] is sealed.
+pub(crate) trait GetSpan {
+    /// Returns the [Span] covering the source code represented by this syntax value.
+    fn span(&self) -> Span;
+}
+
+impl GetSpan for Span {
+    fn span(&self) -> Span {
+        *self
+    }
+}
+
+impl<'a, T: GetSpan> GetSpan for &'a T {
+    fn span(&self) -> Span {
+        (*self).span()
+    }
+}
+
+impl<T: GetSpan> GetSpan for Rc<T> {
+    fn span(&self) -> Span {
+        self.deref().span()
+    }
+}
+
+/// Returns the span of a punctuated sequence of values which implement [GetSpan].
+fn punctuated_span<T: GetSpan, P>(arg: &Punctuated<T, P>) -> Span {
+    let mut iter = arg.into_iter();
+    let mut span = if let Some(state) = iter.next() {
+        state.span()
+    } else {
+        return Span::call_site();
+    };
+    for state in iter {
+        span = span.left_join(state.span());
+    }
+    span
+}
+
+pub(crate) trait MaybeGetSpan {
+    fn maybe_span(&self) -> Option<Span>;
+}
+
+impl<'a, T: MaybeGetSpan> MaybeGetSpan for &'a T {
+    fn maybe_span(&self) -> Option<Span> {
+        (*self).maybe_span()
+    }
+}
+
+impl<T: GetSpan> MaybeGetSpan for Option<T> {
+    fn maybe_span(&self) -> Option<Span> {
+        self.as_ref().map(|x| x.span())
+    }
+}
+
+impl MaybeGetSpan for Span {
+    fn maybe_span(&self) -> Option<Span> {
+        Some(*self)
+    }
+}
+
+trait LeftJoin<Rhs> {
+    /// Attempts to join two [GetSpan] values, but if the result of the join is `None`, then just
+    /// the left span is returned.
+    fn left_join(&self, other: Rhs) -> Span;
+}
+
+impl<R: MaybeGetSpan> LeftJoin<R> for Span {
+    fn left_join(&self, other: R) -> Span {
+        if let Some(span) = other.maybe_span() {
+            self.join(span).unwrap_or(*self)
+        } else {
+            *self
+        }
+    }
+}
+
+/// Returns a new span whose text content is the given string and whose span is `Span::call_site`.
+#[cfg(test)]
+pub(crate) fn new_ident(str: &str) -> Ident {
+    Ident::new(str, Span::call_site())
+}
+
+trait ParsePunctuatedList: Sized {
+    fn parse_list(
+        input: ParseStream,
+        should_break: impl Fn(ParseStream) -> bool,
+    ) -> syn::Result<Self>;
+}
+
+impl<T: Parse, U: Parse> ParsePunctuatedList for Punctuated<Rc<T>, U> {
+    fn parse_list(
+        input: ParseStream,
+        should_break: impl Fn(ParseStream) -> bool,
+    ) -> syn::Result<Self> {
+        let mut output = Punctuated::new();
+        while !input.cursor().eof() {
+            if should_break(input) {
+                break;
+            }
+            output.push_value(Rc::new(input.parse()?));
+            if let Ok(punct) = input.parse() {
+                output.push_punct(punct);
+            }
+        }
+        Ok(output)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use syn::parse_str;
+
+    #[test]
+    fn protocol_parse_minimal() {
+        const EXPECTED_ACTOR: &str = "server";
+        const EXPECTED_NAME: &str = "Foo";
+        const EXPECTED_STATES: [&str; 3] = ["First", "Second", "Third"];
+        let input = format!(
+            "named {EXPECTED_NAME};
+let {EXPECTED_ACTOR} = [{}];
+{} -> {};
+{} -> {};",
+            EXPECTED_STATES.join(", "),
+            EXPECTED_STATES[0],
+            EXPECTED_STATES[1],
+            EXPECTED_STATES[1],
+            EXPECTED_STATES[2],
+        );
+        let expected = Protocol::new(
+            NameDef::new(EXPECTED_NAME),
+            [ActorDef::new(EXPECTED_ACTOR, EXPECTED_STATES)],
+            [
+                Transition::new(
+                    State::new(EXPECTED_STATES[0], []),
+                    None,
+                    [State::new(EXPECTED_STATES[1], [])],
+                    [],
+                ),
+                Transition::new(
+                    State::new(EXPECTED_STATES[1], []),
+                    None,
+                    [State::new(EXPECTED_STATES[2], [])],
+                    [],
+                ),
+            ],
+        );
+
+        let actual = parse_str::<Protocol>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn protocol_parse_with_version() {
+        const EXPECTED_VERSION: u64 = 37;
+        let input = format!(
+            "named VersionTest;
+version {EXPECTED_VERSION};
+let actor = [Init];
+Init?Activate -> End;"
+        );
+        let expected = Protocol::with_version(
+            NameDef::new("VersionTest"),
+            VersionDef::new(EXPECTED_VERSION),
+            [ActorDef::new("actor", ["Init"])],
+            [Transition::new(
+                State::new("Init", []),
+                Some(Message::new("Activate", false, [])),
+                [State::new("End", [])],
+                [],
+            )],
+        );
+
+        let actual = parse_str::<Protocol>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn protocol_parse_version_mismatch_is_err() {
+        const EXPECTED_VERSION: u64 = 37;
+        let input = format!(
+            "named VersionTest;
+version {EXPECTED_VERSION};
+let actor = [Init];
+Init?Activate -> End;"
+        );
+        let expected = Protocol::with_version(
+            NameDef::new("VersionTest"),
+            VersionDef::new(EXPECTED_VERSION + 1),
+            [ActorDef::new("actor", ["Init"])],
+            [Transition::new(
+                State::new("Init", []),
+                Some(Message::new("Activate", false, [])),
+                [State::new("End", [])],
+                [],
+            )],
+        );
+
+        let actual = parse_str::<Protocol>(&input).unwrap();
+
+        assert_ne!(expected, actual);
+    }
+
+    #[test]
+    fn name_def_parse() {
+        const EXPECTED_NAME: &str = "Foofercorg";
+        let input = format!("named {EXPECTED_NAME}");
+        let expected = NameDef::new(EXPECTED_NAME);
+
+        let actual = parse_str::<NameDef>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn name_def_wrong_ident_err() {
+        let result = parse_str::<NameDef>("nmaed Shodan");
+
+        assert!(result.is_err());
+        let err_str = result.err().unwrap().to_string();
+        assert_eq!(NameDef::NAME_IDENT_ERR, err_str);
+    }
+
+    #[test]
+    fn version_def_parse() {
+        const EXPECTED_VERSION: u64 = 1;
+        let input = format!("version {EXPECTED_VERSION}");
+        let expected = VersionDef::new(EXPECTED_VERSION);
+
+        let actual = parse_str::<VersionDef>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn version_def_parse_wrong_ident_err() {
+        let result = parse_str::<VersionDef>("versoin 3");
+
+        assert!(result.is_err());
+        let err_str = result.err().unwrap().to_string();
+        assert_eq!(VersionDef::VERSION_IDENT_ERR, err_str);
+    }
+
+    #[test]
+    fn actor_def_parse() {
+        const EXPECTED_ACTOR: &str = "server";
+        const EXPECTED_STATES: [&str; 2] = ["First", "Second"];
+        let input = format!("let {EXPECTED_ACTOR} = [{}]", EXPECTED_STATES.join(", "));
+        let expected = ActorDef::new(EXPECTED_ACTOR, EXPECTED_STATES);
+
+        let actual = parse_str::<ActorDef>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn ident_array_new() {
+        const EXPECTED: [&str; 2] = ["Red", "Green"];
+
+        let actual = IdentArray::new(EXPECTED).unwrap();
+
+        assert_eq!(EXPECTED.len(), actual.idents.len());
+        assert_eq!(actual.idents[0].as_ref(), EXPECTED[0]);
+        assert_eq!(actual.idents[1].as_ref(), EXPECTED[1]);
+    }
+
+    #[test]
+    fn ident_array_not_equal() {
+        let expected = IdentArray::new(["Red", "Green"]);
+
+        let actual = IdentArray::new(["Blue", "Gold"]);
+
+        assert_ne!(expected, actual);
+    }
+
+    #[test]
+    fn ident_array_not_equal_different_lens() {
+        let expected = IdentArray::new(["Red", "Green"]);
+
+        let actual = IdentArray::new(["Red"]);
+
+        assert_ne!(expected, actual);
+    }
+
+    #[test]
+    fn ident_array_parse() {
+        const EXPECTED_STATES: [&str; 2] = ["Sad", "Glad"];
+        let input = format!("[{}, {}]", EXPECTED_STATES[0], EXPECTED_STATES[1]);
+        let expected = IdentArray::new(EXPECTED_STATES).unwrap();
+
+        let actual = parse_str::<IdentArray>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn ident_array_parse_empty_err() {
+        let result = parse_str::<IdentArray>("[]");
+
+        assert!(result.is_err());
+        let err_str = result.err().unwrap().to_string();
+        assert_eq!(IdentArray::EMPTY_ERR, err_str);
+    }
+
+    #[test]
+    fn transition_parse_minimal() {
+        const EXPECTED_IN_STATE: &str = "Catcher";
+        const EXPECTED_OUT_STATE: &str = "End";
+        let input = format!("{EXPECTED_IN_STATE} -> {EXPECTED_OUT_STATE}");
+        let expected = Transition::new(
+            State::new(EXPECTED_IN_STATE, []),
+            None,
+            [EXPECTED_OUT_STATE].map(State::new_empty_owned),
+            [],
+        );
+
+        let actual = parse_str::<Transition>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn transition_parse_with_in_msg() {
+        const EXPECTED_IN_STATE: &str = "Catcher";
+        const EXPECTED_IN_MSG: &str = "Msg";
+        const EXPECTED_OUT_STATE: &str = "End";
+        let input = format!("{EXPECTED_IN_STATE}?{EXPECTED_IN_MSG} -> {EXPECTED_OUT_STATE}");
+        let expected = Transition::new(
+            State::new(EXPECTED_IN_STATE, []),
+            Some(Message::new(EXPECTED_IN_MSG, false, [])),
+            [EXPECTED_OUT_STATE].map(State::new_empty_owned),
+            [],
+        );
+
+        let actual = parse_str::<Transition>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn transition_parse_with_in_msg_reply() {
+        const EXPECTED_IN_STATE: &str = "Catcher";
+        const EXPECTED_IN_MSG: &str = "Msg";
+        const EXPECTED_OUT_STATE: &str = "End";
+        let input = format!("{EXPECTED_IN_STATE}?{EXPECTED_IN_MSG}::Reply -> {EXPECTED_OUT_STATE}");
+        let expected = Transition::new(
+            State::new(EXPECTED_IN_STATE, []),
+            Some(Message::new(EXPECTED_IN_MSG, true, [])),
+            [EXPECTED_OUT_STATE].map(State::new_empty_owned),
+            [],
+        );
+
+        let actual = parse_str::<Transition>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn transition_parse_with_in_msg_reply_owned_states() {
+        const EXPECTED_IN_STATE: &str = "Catcher";
+        const EXPECTED_IN_MSG: &str = "Msg";
+        const EXPECTED_OWNED_STATES: [&str; 2] = ["First", "Second"];
+        const EXPECTED_OUT_STATE: &str = "End";
+        let input = format!(
+            "{EXPECTED_IN_STATE}?{EXPECTED_IN_MSG}::Reply[{}, {}] -> {EXPECTED_OUT_STATE}",
+            EXPECTED_OWNED_STATES[0], EXPECTED_OWNED_STATES[1]
+        );
+        let expected = Transition::new(
+            State::new(EXPECTED_IN_STATE, []),
+            Some(Message::new(EXPECTED_IN_MSG, true, EXPECTED_OWNED_STATES)),
+            [EXPECTED_OUT_STATE].map(State::new_empty_owned),
+            [],
+        );
+
+        let actual = parse_str::<Transition>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn transition_parse_has_single_dest() {
+        const EXPECTED_IN_STATE: &str = "Catcher";
+        const EXPECTED_OUT_STATE: &str = "End";
+        const EXPECTED_DEST_STATE: &str = "Target";
+        const EXPECTED_DEST_MSG: &str = "DeathNote";
+        let input = format!("{EXPECTED_IN_STATE} -> {EXPECTED_OUT_STATE} >{EXPECTED_DEST_STATE}!{EXPECTED_DEST_MSG}");
+        let expected = Transition::new(
+            State::new(EXPECTED_IN_STATE, []),
+            None,
+            [EXPECTED_OUT_STATE].map(State::new_empty_owned),
+            [Dest::new(
+                DestinationState::Individual(State::new(EXPECTED_DEST_STATE, [])),
+                Message::new(EXPECTED_DEST_MSG, false, []),
+            )],
+        );
+
+        let actual = parse_str::<Transition>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn transition_parse_multiple_out_states_and_out_msgs() {
+        const EXPECTED_IN_STATE: &str = "Catcher";
+        const EXPECTED_IN_MSG: &str = "Tap";
+        const EXPECTED_OUT_STATES: [&str; 2] = ["Out0", "Out1"];
+        const EXPECTED_DESTS: [(&str, &str); 2] = [("Dest0", "Msg0"), ("Dest1", "Msg1")];
+        let states_list = EXPECTED_OUT_STATES.join(", ");
+        let dest_list = EXPECTED_DESTS.map(|(l, r)| format!("{l}!{r}")).join(", ");
+        let input = format!("{EXPECTED_IN_STATE}?{EXPECTED_IN_MSG} -> {states_list} >{dest_list}");
+        let expected = Transition::new(
+            State::new(EXPECTED_IN_STATE, []),
+            Some(Message::new(EXPECTED_IN_MSG, false, [])),
+            EXPECTED_OUT_STATES.map(State::new_empty_owned),
+            EXPECTED_DESTS.map(|(l, r)| {
+                Dest::new(
+                    DestinationState::Individual(State::new(l, [])),
+                    Message::new(r, false, []),
+                )
+            }),
+        );
+
+        let actual = parse_str::<Transition>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn state_parse_no_owned_states() {
+        const EXPECTED_TRAIT: &str = "Contained";
+        let expected = State::new(EXPECTED_TRAIT, []);
+
+        let actual = parse_str::<State>(EXPECTED_TRAIT).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn state_parse_has_owned_states() {
+        const EXPECTED_TRAIT: &str = "Contained";
+        const EXPECTED_OWNED: [&str; 2] = ["First", "Second"];
+        let input = format!(
+            "{EXPECTED_TRAIT}[{}, {}]",
+            EXPECTED_OWNED[0], EXPECTED_OWNED[1]
+        );
+        let expected = State::new(EXPECTED_TRAIT, EXPECTED_OWNED);
+
+        let actual = parse_str::<State>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn dest_parse() {
+        const EXPECTED_STATE: &str = "Opened";
+        const EXPECTED_MSG: &str = "Read";
+        let input = format!("{EXPECTED_STATE}!{EXPECTED_MSG}");
+        let expected = Dest::new(
+            DestinationState::Individual(State::new(EXPECTED_STATE, [])),
+            Message::new(EXPECTED_MSG, false, []),
+        );
+
+        let actual = parse_str::<Dest>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn dest_list_trailing_comma_err() {
+        const INPUT: &str = "First!Msg0, Second!Msg1,";
+
+        let result = parse_str::<DestList>(INPUT);
+
+        assert!(result.is_err());
+        let err_str = result.err().unwrap().to_string();
+        assert_eq!(DestList::TRAILING_COMMA_ERR, err_str);
+    }
+
+    #[test]
+    fn destination_state_parse_regular() {
+        const EXPECTED_DEST_STATE: &str = "Listening";
+        let expected = DestinationState::Individual(State::new(EXPECTED_DEST_STATE, []));
+
+        let actual = parse_str::<DestinationState>(EXPECTED_DEST_STATE).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn destination_state_parse_owned_states() {
+        const EXPECTED_DEST_STATE: &str = "Listening";
+        const EXPECTED_OWNED_STATES: [&str; 2] = ["First", "Second"];
+        let input = format!(
+            "{EXPECTED_DEST_STATE}[{}]",
+            EXPECTED_OWNED_STATES.join(", ")
+        );
+        let expected =
+            DestinationState::Individual(State::new(EXPECTED_DEST_STATE, EXPECTED_OWNED_STATES));
+
+        let actual = parse_str::<DestinationState>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn destination_state_parse_service() {
+        const EXPECTED_DEST_STATE: &str = "Listening";
+        let expected = DestinationState::Service(State::new(EXPECTED_DEST_STATE, []));
+        let input = format!("service({EXPECTED_DEST_STATE})");
+
+        let actual = parse_str::<DestinationState>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn destination_state_parse_service_no_state_err() {
+        let result = parse_str::<DestinationState>("service()");
+
+        assert!(result.is_err());
+        let err_str = result.err().unwrap().to_string();
+        assert_eq!(DestinationState::NO_STATE_ERR, err_str);
+    }
+
+    #[test]
+    fn destination_state_parse_service_multi_state_err() {
+        let result = parse_str::<DestinationState>("service(Left, Right)");
+
+        assert!(result.is_err());
+        let err_str = result.err().unwrap().to_string();
+        assert_eq!(DestinationState::MULTI_STATE_ERR, err_str);
+    }
+
+    #[test]
+    fn message_new() {
+        const EXPECTED_MSG_TYPE: &str = "Orders";
+        const EXPECTED_IS_REPLY: bool = true;
+        const EXPECTED_OWNED_STATES: [&str; 2] = ["First", "Second"];
+
+        let actual = Message::new(EXPECTED_MSG_TYPE, EXPECTED_IS_REPLY, EXPECTED_OWNED_STATES);
+
+        assert_eq!(actual.msg_type.as_ref(), EXPECTED_MSG_TYPE);
+        assert_eq!(actual.is_reply(), EXPECTED_IS_REPLY);
+        let idents = actual.owned_states.unwrap().idents;
+        assert_eq!(idents.len(), EXPECTED_OWNED_STATES.len());
+        assert_eq!(idents[0].as_ref(), EXPECTED_OWNED_STATES[0]);
+        assert_eq!(idents[1].as_ref(), EXPECTED_OWNED_STATES[1]);
+    }
+
+    #[test]
+    fn message_parse_regular() {
+        const EXPECTED_MSG: &str = "Write";
+        let expected = Message::new(EXPECTED_MSG, false, []);
+
+        let actual = parse_str::<Message>(EXPECTED_MSG).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn message_parse_reply() {
+        const EXPECTED_MSG: &str = "ReadAttr";
+        let input = format!("{EXPECTED_MSG}::Reply");
+        let expected = Message::new(EXPECTED_MSG, true, []);
+
+        let actual = parse_str::<Message>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn message_parse_reply_err() {
+        const EXPECTED_MSG: &str = "ReadAttr";
+        // Notice the intentional typo: "Rely" instead of "Reply".
+        let input = format!("{EXPECTED_MSG}::Rely");
+
+        let result = parse_str::<Message>(&input);
+
+        assert!(result.is_err());
+        let err_str = result.err().unwrap().to_string();
+        assert_eq!(MessageReplyPart::REPLY_ERR, err_str);
+    }
+
+    #[test]
+    fn message_parse_owned_states_not_reply() {
+        const EXPECTED_MSG: &str = "Open";
+        const OWNED_STATES: [&str; 2] = ["Handle0", "Handle1"];
+        let input = format!("{EXPECTED_MSG}[{}, {}]", OWNED_STATES[0], OWNED_STATES[1]);
+        let expected = Message::new(EXPECTED_MSG, false, OWNED_STATES);
+
+        let actual = parse_str::<Message>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn message_parse_owned_states_is_reply() {
+        const EXPECTED_MSG: &str = "Open";
+        const OWNED_STATES: [&str; 2] = ["Handle0", "Handle1"];
+        let input = format!(
+            "{EXPECTED_MSG}::Reply[{}, {}]",
+            OWNED_STATES[0], OWNED_STATES[1]
+        );
+        let expected = Message::new(EXPECTED_MSG, true, OWNED_STATES);
+
+        let actual = parse_str::<Message>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+}

+ 700 - 0
crates/btproto/src/validation.rs

@@ -0,0 +1,700 @@
+use std::{collections::HashSet, hash::Hash};
+
+use proc_macro2::{Ident, Span};
+
+use btrun::model::End;
+
+use crate::{
+    error::{self, MaybeErr},
+    model::{MsgInfo, ProtocolModel},
+    parsing::{DestinationState, GetSpan, State},
+};
+
+impl ProtocolModel {
+    #[allow(dead_code)]
+    pub(crate) fn validate(&self) -> syn::Result<()> {
+        self.all_states_declared_and_used()
+            .combine(self.receivers_and_senders_matched())
+            .combine(self.no_undeliverable_msgs())
+            .combine(self.replies_expected())
+            .combine(self.no_unobservable_states())
+            .into()
+    }
+
+    /// Verifies that every state which is declared is actually used.
+    fn all_states_declared_and_used(&self) -> MaybeErr {
+        let end = Ident::new(End::ident(), Span::call_site());
+        let mut declared: HashSet<&Ident> = HashSet::new();
+        declared.insert(&end);
+        for actor_def in self.def().actor_defs.iter() {
+            for state in actor_def.states.as_ref().iter() {
+                declared.insert(state);
+            }
+        }
+        let mut used: HashSet<&Ident> = HashSet::with_capacity(declared.len());
+        for transition in self.def().transitions.iter() {
+            let in_state = &transition.in_state;
+            used.insert(&in_state.state_trait);
+            used.extend(in_state.owned_states().map(|ident| ident.as_ref()));
+            if let Some(in_msg) = transition.in_msg() {
+                used.extend(in_msg.owned_states().map(|ident| ident.as_ref()));
+            }
+            for out_states in transition.out_states.as_ref().iter() {
+                used.insert(&out_states.state_trait);
+                used.extend(out_states.owned_states().map(|ident| ident.as_ref()));
+            }
+            // We don't have to check the states referred to in out_msgs because the
+            // receivers_and_senders_matched method ensures that each of these exists in a receiver
+            // position.
+        }
+        let undeclared: MaybeErr = used
+            .difference(&declared)
+            .map(|ident| syn::Error::new(ident.span(), error::msgs::UNDECLARED_STATE))
+            .collect();
+        let unused: MaybeErr = declared
+            .difference(&used)
+            .filter(|ident| **ident != End::ident())
+            .map(|ident| syn::Error::new(ident.span(), error::msgs::UNUSED_STATE))
+            .collect();
+        undeclared.combine(unused)
+    }
+
+    /// Ensures that the recipient state for every sent message has a receiving transition
+    /// defined, and every receiver has a sender. Note that each message isn't required to have a
+    /// unique sender or a unique receiver, just that at least one of each much be defined.
+    fn receivers_and_senders_matched<'s>(&'s self) -> MaybeErr {
+        /// Represents a message sender or receiver.
+        ///
+        /// This type is essentially just a tuple of references, but was created so a [Hash]
+        /// implementation could be defined.
+        #[cfg_attr(test, derive(Debug))]
+        struct MsgEndpoint<'a> {
+            state: &'a State,
+            msg_info: &'a MsgInfo,
+        }
+
+        impl<'a> MsgEndpoint<'a> {
+            fn new(state: &'a State, msg_info: &'a MsgInfo) -> Self {
+                Self { state, msg_info }
+            }
+        }
+
+        impl<'a> PartialEq for MsgEndpoint<'a> {
+            fn eq(&self, other: &Self) -> bool {
+                self.state.state_trait == other.state.state_trait
+                    && self.msg_info.def().msg_type == self.msg_info.def().msg_type
+                    && self.msg_info.is_reply() == self.msg_info.is_reply()
+            }
+        }
+
+        impl<'a> Eq for MsgEndpoint<'a> {}
+
+        impl<'a> Hash for MsgEndpoint<'a> {
+            fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
+                self.state.state_trait.hash(state);
+                self.msg_info.def().msg_type.hash(state);
+                self.msg_info.is_reply().hash(state);
+            }
+        }
+
+        #[cfg(test)]
+        impl<'a> std::fmt::Display for MsgEndpoint<'a> {
+            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+                write!(
+                    f,
+                    "({}, {})",
+                    self.state.state_trait,
+                    self.msg_info.msg_name()
+                )
+            }
+        }
+
+        let msgs = self.msg_lookup();
+        let mut outgoing: HashSet<MsgEndpoint<'s>> = HashSet::new();
+        let mut incoming: HashSet<MsgEndpoint<'s>> = HashSet::new();
+        for actor in self.actors_iter() {
+            let methods = actor
+                .states()
+                .values()
+                .flat_map(|state| state.methods().values());
+            for method in methods {
+                let transition = method.def();
+                if let Some(msg) = transition.in_msg() {
+                    let msg_info = msgs.lookup(msg);
+                    incoming.insert(MsgEndpoint::new(&transition.in_state, msg_info));
+                }
+                for dest in transition.out_msgs.as_ref().iter() {
+                    let dest_state = match &dest.state {
+                        DestinationState::Individual(dest_state) => dest_state,
+                        DestinationState::Service(dest_state) => dest_state,
+                    };
+                    let msg_info = self.msg_lookup().lookup(&dest.msg);
+                    outgoing.insert(MsgEndpoint::new(dest_state, msg_info));
+                    if actor.is_client() {
+                        if let Some(reply) = msg_info.reply() {
+                            incoming.insert(MsgEndpoint::new(&transition.in_state, reply));
+                        }
+                    }
+                }
+            }
+        }
+        let extra_senders: MaybeErr = outgoing
+            .difference(&incoming)
+            .map(|endpoint| {
+                syn::Error::new(
+                    endpoint.msg_info.def().span(),
+                    error::msgs::UNMATCHED_OUTGOING,
+                )
+            })
+            .collect();
+        let extra_receivers: MaybeErr = incoming
+            .difference(&outgoing)
+            .map(|endpoint| {
+                syn::Error::new(
+                    endpoint.msg_info.def().span(),
+                    error::msgs::UNMATCHED_INCOMING,
+                )
+            })
+            .collect();
+        extra_senders.combine(extra_receivers)
+    }
+
+    /// Checks that messages are only sent to destinations which are either services, states
+    /// which are owned by the sender, listed in the output states, or that the message is a
+    /// reply.
+    fn no_undeliverable_msgs(&self) -> MaybeErr {
+        let mut err = MaybeErr::none();
+        for transition in self.def().transitions.iter() {
+            let mut allowed_states: Option<HashSet<&Ident>> = None;
+            for dest in transition.out_msgs.as_ref().iter() {
+                if dest.msg.is_reply() {
+                    continue;
+                }
+                match &dest.state {
+                    DestinationState::Service(_) => continue,
+                    DestinationState::Individual(dest_state) => {
+                        let owned_states = transition
+                            .in_state
+                            .owned_states()
+                            .map(|ident| ident.as_ref());
+                        let allowed = allowed_states.get_or_insert_with(|| {
+                            transition
+                                .out_states
+                                .as_ref()
+                                .iter()
+                                .map(|state| state.state_trait.as_ref())
+                                .chain(owned_states)
+                                .collect()
+                        });
+                        if !allowed.contains(dest_state.state_trait.as_ref()) {
+                            err = err.combine(
+                                syn::Error::new(
+                                    dest_state.state_trait.span(),
+                                    error::msgs::UNDELIVERABLE,
+                                )
+                                .into(),
+                            );
+                        }
+                    }
+                }
+            }
+        }
+        err
+    }
+
+    /// Verifies that exactly one reply is sent in response to a previously sent message.
+    fn replies_expected(&self) -> MaybeErr {
+        let mut err = MaybeErr::none();
+        for transition in self.def().transitions.iter() {
+            let replies: Vec<_> = transition
+                .out_msgs
+                .as_ref()
+                .iter()
+                .map(|dest| &dest.msg)
+                .filter(|msg| msg.is_reply())
+                .collect();
+            if replies.is_empty() {
+                continue;
+            }
+            if replies.len() > 1 {
+                err = err.combine(
+                    replies
+                        .iter()
+                        .map(|reply| {
+                            syn::Error::new(reply.msg_type.span(), error::msgs::MULTIPLE_REPLIES)
+                        })
+                        .collect(),
+                );
+            }
+            if transition.in_msg().is_none() {
+                err = err.combine(
+                    replies
+                        .iter()
+                        .map(|reply| {
+                            syn::Error::new(reply.msg_type.span(), error::msgs::INVALID_REPLY)
+                        })
+                        .collect(),
+                );
+            }
+        }
+        err
+    }
+
+    /// Checks that there are no client states which are only receiving replies. Such states can't
+    /// be observed, because the methods which sent the original messages will return their replies.
+    fn no_unobservable_states(&self) -> MaybeErr {
+        self.actors_iter()
+            .filter(|actor| actor.is_client())
+            .flat_map(|actor| actor.states().values())
+            .filter(|state| {
+                state.methods().values().all(|method| {
+                    if let Some(in_msg) = method.def().in_msg() {
+                        in_msg.is_reply()
+                    } else {
+                        false
+                    }
+                })
+            })
+            .map(|state| syn::Error::new(state.span(), error::msgs::UNOBSERVABLE_STATE))
+            .collect()
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::{
+        error::{assert_err, assert_ok},
+        parsing::{ActorDef, Dest, Message, NameDef, Protocol, Transition},
+    };
+
+    #[test]
+    fn all_states_declared_and_used_ok() {
+        let input = ProtocolModel::new(Protocol::minimal()).unwrap();
+
+        let result = input.all_states_declared_and_used();
+
+        assert_ok(result);
+    }
+
+    #[test]
+    fn all_states_declared_and_used_end_not_used_ok() {
+        const STATE_NAME: &str = "Init";
+        let input = ProtocolModel::new(Protocol::new(
+            NameDef::new("Test"),
+            [ActorDef::new("actor", [STATE_NAME])],
+            [Transition::new(
+                State::new(STATE_NAME, []),
+                Some(Message::new("Activate", false, [])),
+                [State::new(STATE_NAME, [])],
+                [],
+            )],
+        ))
+        .unwrap();
+
+        let result = input.all_states_declared_and_used();
+
+        assert_ok(result);
+    }
+
+    #[test]
+    fn all_states_declared_and_used_undeclared_err() {
+        let input = ProtocolModel::new(Protocol::new(
+            NameDef::new("Undeclared"),
+            [ActorDef::new("actor", ["Init"])],
+            [Transition::new(
+                State::new("Init", []),
+                Some(Message::new("Activate", false, [])),
+                [State::new("Next", [])],
+                [],
+            )],
+        ))
+        .unwrap();
+
+        let result = input.all_states_declared_and_used();
+
+        assert_err(result, error::msgs::UNDECLARED_STATE);
+    }
+
+    #[test]
+    fn all_states_declared_and_used_undeclared_out_state_owned_err() {
+        let input = ProtocolModel::new(Protocol::new(
+            NameDef::new("Undeclared"),
+            [ActorDef::new("actor", ["Init", "Next"])],
+            [Transition::new(
+                State::new("Init", []),
+                Some(Message::new("Activate", false, [])),
+                [State::new("Init", []), State::new("Next", ["Undeclared"])],
+                [],
+            )],
+        ))
+        .unwrap();
+
+        let result = input.all_states_declared_and_used();
+
+        assert_err(result, error::msgs::UNDECLARED_STATE);
+    }
+
+    #[test]
+    fn all_states_declared_and_used_undeclared_in_state_owned_err() {
+        let input = ProtocolModel::new(Protocol::new(
+            NameDef::new("Undeclared"),
+            [ActorDef::new("actor", ["Init", "Next"])],
+            [Transition::new(
+                State::new("Init", ["Undeclared"]),
+                Some(Message::new("Activate", false, [])),
+                [State::new("Next", [])],
+                [],
+            )],
+        ))
+        .unwrap();
+
+        let result = input.all_states_declared_and_used();
+
+        assert_err(result, error::msgs::UNDECLARED_STATE);
+    }
+
+    #[test]
+    fn all_states_declared_and_used_unused_err() {
+        let input = ProtocolModel::new(Protocol::new(
+            NameDef::new("Unused"),
+            [ActorDef::new("actor", ["Init", "Extra"])],
+            [Transition::new(
+                State::new("Init", []),
+                Some(Message::new("Activate", false, [])),
+                [State::new("End", [])],
+                [],
+            )],
+        ))
+        .unwrap();
+
+        let result = input.all_states_declared_and_used();
+
+        assert_err(result, error::msgs::UNUSED_STATE);
+    }
+
+    #[test]
+    fn receivers_and_senders_matched_ok() {
+        let input = ProtocolModel::new(Protocol::minimal()).unwrap();
+
+        let result = input.receivers_and_senders_matched();
+
+        assert_ok(result);
+    }
+
+    #[test]
+    fn receivers_and_senders_call_msg_reused_for_non_call_ok() {
+        let input = ProtocolModel::new(Protocol::new(
+            NameDef::new("OwnedTypes"),
+            [
+                ActorDef::new("server", ["Listening"]),
+                ActorDef::new("client", ["Client"]),
+                ActorDef::new("file", ["FileInit", "Opened"]),
+                ActorDef::new("file_handle", ["FileHandle"]),
+            ],
+            [
+                Transition::new(
+                    State::new("Client", []),
+                    None,
+                    [
+                        State::new("Client", []),
+                        State::new("FileHandle", ["Opened"]),
+                    ],
+                    [Dest::new(
+                        DestinationState::Service(State::new("Listening", [])),
+                        Message::new("Open", false, []),
+                    )],
+                ),
+                Transition::new(
+                    State::new("Listening", []),
+                    Some(Message::new("Open", false, [])),
+                    [State::new("Listening", []), State::new("FileInit", [])],
+                    [
+                        Dest::new(
+                            DestinationState::Individual(State::new("Client", [])),
+                            Message::new("Open", true, ["Opened"]),
+                        ),
+                        // Note that the same "Open" message is being used here, but that the
+                        // FileInit state does not send a reply. This should be allowed.
+                        Dest::new(
+                            DestinationState::Individual(State::new("FileInit", [])),
+                            Message::new("Open", false, []),
+                        ),
+                    ],
+                ),
+                Transition::new(
+                    State::new("FileInit", []),
+                    Some(Message::new("Open", false, [])),
+                    [State::new("Opened", [])],
+                    [],
+                ),
+            ],
+        ))
+        .unwrap();
+
+        let result = input.receivers_and_senders_matched();
+
+        assert_ok(result);
+    }
+
+    #[test]
+    fn receivers_and_senders_matched_unmatched_sender_err() {
+        let input = ProtocolModel::new(Protocol::new(
+            NameDef::new("Unbalanced"),
+            [ActorDef::new("actor", ["Init"])],
+            [Transition::new(
+                State::new("Init", []),
+                None,
+                [State::new("Init", [])],
+                [Dest::new(
+                    DestinationState::Service(State::new("Init", [])),
+                    Message::new("Msg", false, []),
+                )],
+            )],
+        ))
+        .unwrap();
+
+        let result = input.receivers_and_senders_matched();
+
+        assert_err(result, error::msgs::UNMATCHED_OUTGOING);
+    }
+
+    #[test]
+    fn receivers_and_senders_matched_unmatched_receiver_err() {
+        let input = ProtocolModel::new(Protocol::new(
+            NameDef::new("Unbalanced"),
+            [ActorDef::new("actor", ["Init"])],
+            [Transition::new(
+                State::new("Init", []),
+                Some(Message::new("NotExists", false, [])),
+                [State::new("Init", [])],
+                [],
+            )],
+        ))
+        .unwrap();
+
+        let result = input.receivers_and_senders_matched();
+
+        assert_err(result, error::msgs::UNMATCHED_INCOMING);
+    }
+
+    #[test]
+    fn receivers_and_senders_matched_servers_must_explicitly_receive_replies_err() {
+        // Only client actors are allowed to implicitly receive replies.
+        let input = ProtocolModel::new(Protocol::new(
+            NameDef::new("Conversation"),
+            [
+                ActorDef::new("alice", ["Alice"]),
+                ActorDef::new("bob", ["Bob"]),
+            ],
+            [
+                Transition::new(
+                    State::new("Alice", []),
+                    None,
+                    [State::new("Alice", [])],
+                    [Dest::new(
+                        DestinationState::Service(State::new("Bob", [])),
+                        Message::new("Greeting", false, []),
+                    )],
+                ),
+                // Notice that because Bob only has transitions which handle messages, bob is a
+                // server actor.
+                Transition::new(
+                    State::new("Bob", []),
+                    Some(Message::new("Greeting", false, [])),
+                    [State::new("Bob", [])],
+                    [Dest::new(
+                        DestinationState::Individual(State::new("Alice", [])),
+                        Message::new("Query", false, []),
+                    )],
+                ),
+                // Alice is sending a Query::Reply to Bob, but because he does not have a
+                // transition which accepts that message type, this will be an unmatched outgoing
+                // error.
+                Transition::new(
+                    State::new("Alice", []),
+                    Some(Message::new("Query", false, [])),
+                    [State::new("End", [])],
+                    [Dest::new(
+                        DestinationState::Individual(State::new("Bob", [])),
+                        Message::new("Query", true, []),
+                    )],
+                ),
+            ],
+        ))
+        .unwrap();
+
+        let result = input.receivers_and_senders_matched();
+
+        assert_err(result, error::msgs::UNMATCHED_OUTGOING);
+    }
+
+    #[test]
+    fn no_undeliverable_msgs_ok() {
+        let input = ProtocolModel::new(Protocol::minimal()).unwrap();
+
+        let result = input.no_undeliverable_msgs();
+
+        assert_ok(result);
+    }
+
+    #[test]
+    fn no_undeliverable_msgs_reply_ok() {
+        let input = ProtocolModel::new(Protocol::new(
+            NameDef::new("Undeliverable"),
+            [ActorDef::new("actor", ["Listening", "Client"])],
+            [Transition::new(
+                State::new("Listening", []),
+                Some(Message::new("Msg", false, [])),
+                [State::new("Listening", [])],
+                [Dest::new(
+                    DestinationState::Individual(State::new("Client", [])),
+                    Message::new("Msg", true, []),
+                )],
+            )],
+        ))
+        .unwrap();
+
+        let result = input.no_undeliverable_msgs();
+
+        assert_ok(result);
+    }
+
+    #[test]
+    fn no_undeliverable_msgs_service_ok() {
+        let input = ProtocolModel::new(Protocol::new(
+            NameDef::new("Undeliverable"),
+            [ActorDef::new("actor", ["Client", "Server"])],
+            [Transition::new(
+                State::new("Client", []),
+                None,
+                [State::new("Client", [])],
+                [Dest::new(
+                    DestinationState::Service(State::new("Server", [])),
+                    Message::new("Msg", false, []),
+                )],
+            )],
+        ))
+        .unwrap();
+
+        let result = input.no_undeliverable_msgs();
+
+        assert_ok(result);
+    }
+
+    #[test]
+    fn no_undeliverable_msgs_owned_ok() {
+        let input = ProtocolModel::new(Protocol::new(
+            NameDef::new("Undeliverable"),
+            [ActorDef::new("actor", ["FileClient", "FileHandle"])],
+            [Transition::new(
+                State::new("FileClient", ["FileHandle"]),
+                None,
+                [State::new("FileClient", [])],
+                [Dest::new(
+                    DestinationState::Individual(State::new("FileHandle", [])),
+                    Message::new("FileOp", false, []),
+                )],
+            )],
+        ))
+        .unwrap();
+
+        let result = input.no_undeliverable_msgs();
+
+        assert_ok(result);
+    }
+
+    #[test]
+    fn no_undeliverable_msgs_err() {
+        let input = ProtocolModel::new(Protocol::new(
+            NameDef::new("Undeliverable"),
+            [ActorDef::new("actor", ["Client", "Server"])],
+            [Transition::new(
+                State::new("Client", []),
+                None,
+                [State::new("Client", [])],
+                [Dest::new(
+                    DestinationState::Individual(State::new("Server", [])),
+                    Message::new("Msg", false, []),
+                )],
+            )],
+        ))
+        .unwrap();
+
+        let result = input.no_undeliverable_msgs();
+
+        assert_err(result, error::msgs::UNDELIVERABLE);
+    }
+
+    #[test]
+    fn replies_expected_ok() {
+        let input = ProtocolModel::new(Protocol::new(
+            NameDef::new("ValidReplies"),
+            [ActorDef::new("actor", ["Client", "Server"])],
+            [Transition::new(
+                State::new("Server", []),
+                Some(Message::new("Msg", false, [])),
+                [State::new("Server", [])],
+                [Dest::new(
+                    DestinationState::Individual(State::new("Client", [])),
+                    Message::new("Msg", true, []),
+                )],
+            )],
+        ))
+        .unwrap();
+
+        let result = input.replies_expected();
+
+        assert_ok(result);
+    }
+
+    #[test]
+    fn replies_expected_invalid_reply_err() {
+        let input = ProtocolModel::new(Protocol::new(
+            NameDef::new("ValidReplies"),
+            [ActorDef::new("actor", ["Client", "Server"])],
+            [Transition::new(
+                State::new("Client", []),
+                None,
+                [State::new("Client", [])],
+                [Dest::new(
+                    DestinationState::Individual(State::new("Server", [])),
+                    Message::new("Msg", true, []),
+                )],
+            )],
+        ))
+        .unwrap();
+
+        let result = input.replies_expected();
+
+        assert_err(result, error::msgs::INVALID_REPLY);
+    }
+
+    #[test]
+    fn replies_expected_multiple_replies_err() {
+        let input = ProtocolModel::new(Protocol::new(
+            NameDef::new("ValidReplies"),
+            [ActorDef::new("actor", ["Client", "OtherClient", "Server"])],
+            [Transition::new(
+                State::new("Server", []),
+                Some(Message::new("Msg", false, [])),
+                [State::new("Server", [])],
+                [
+                    Dest::new(
+                        DestinationState::Individual(State::new("Client", [])),
+                        Message::new("Msg", true, []),
+                    ),
+                    Dest::new(
+                        DestinationState::Individual(State::new("OtherClient", [])),
+                        Message::new("Msg", true, []),
+                    ),
+                ],
+            )],
+        ))
+        .unwrap();
+
+        let result = input.replies_expected();
+
+        assert_err(result, error::msgs::MULTIPLE_REPLIES);
+    }
+}

+ 190 - 0
crates/btproto/tests/protocol_tests.rs

@@ -0,0 +1,190 @@
+#![feature(impl_trait_in_assoc_type)]
+
+use std::future::{ready, Ready};
+
+use btproto::protocol;
+use btrun::model::{CallMsg, End, TransResult};
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize)]
+pub struct Ping;
+
+impl CallMsg for Ping {
+    type Reply = ();
+}
+
+/// Tests that the given variable is of the given type _at compile time_.
+macro_rules! assert_type {
+    ($var:expr, $ty:ty) => {{
+        let _: $ty = $var;
+    }};
+}
+
+#[test]
+fn minimal_syntax() {
+    #[derive(Serialize, Deserialize)]
+    pub struct Msg;
+
+    protocol! {
+        named MinimalTest;
+        let server = [Server];
+        let client = [Client];
+        Client -> End, >service(Server)!Msg;
+        Server?Msg -> End;
+    }
+
+    let msg: Option<MinimalTestMsgs> = None;
+    match msg {
+        Some(MinimalTestMsgs::Msg(act)) => assert_type!(act, Msg),
+        None => (),
+    }
+
+    struct ServerState;
+
+    impl Server for ServerState {
+        fn actor_impl() -> String {
+            "server".into()
+        }
+
+        type HandleMsgFut = Ready<TransResult<Self, End>>;
+        fn handle_msg(self, _msg: Msg) -> Self::HandleMsgFut {
+            ready(TransResult::Ok(End))
+        }
+    }
+
+    struct ClientState;
+
+    impl Client for ClientState {
+        fn actor_impl() -> String {
+            "client".into()
+        }
+
+        type OnSendMsgFut = Ready<TransResult<Self, End>>;
+        fn on_send_msg(self, _msg: &mut Msg) -> Self::OnSendMsgFut {
+            ready(TransResult::Ok(End))
+        }
+    }
+}
+
+#[test]
+fn reply() {
+    protocol! {
+        named ReplyTest;
+        let server = [Listening];
+        let client = [Client];
+        Client -> Client, >service(Listening)!Ping;
+        Listening?Ping -> Listening, >Client!Ping::Reply;
+    }
+
+    let msg: Option<ReplyTestMsgs> = None;
+    match msg {
+        Some(ReplyTestMsgs::Ping(ping)) => assert_type!(ping, Ping),
+        Some(ReplyTestMsgs::PingReply(reply)) => assert_type!(reply, <Ping as CallMsg>::Reply),
+        None => (),
+    }
+
+    struct ListeningState;
+
+    impl Listening for ListeningState {
+        fn actor_impl() -> String {
+            "server".into()
+        }
+
+        type HandlePingListening = Self;
+        type HandlePingFut = Ready<TransResult<Self, (Self, <Ping as CallMsg>::Reply)>>;
+        fn handle_ping(self, _msg: Ping) -> Self::HandlePingFut {
+            ready(TransResult::Ok((self, ())))
+        }
+    }
+
+    struct ClientState;
+
+    impl Client for ClientState {
+        fn actor_impl() -> String {
+            "client".into()
+        }
+
+        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, ())))
+        }
+    }
+}
+
+#[test]
+fn client_callback() {
+    #[derive(Serialize, Deserialize)]
+    pub struct Register;
+    #[derive(Serialize, Deserialize)]
+    pub struct Completed;
+
+    protocol! {
+        named ClientCallback;
+        let server = [Listening];
+        let worker = [Working];
+        let client = [Unregistered, Registered];
+        Unregistered -> Registered, >service(Listening)!Register[Registered];
+        Listening?Register[Registered] -> Listening, Working[Registered];
+        Working[Registered] -> End, >Registered!Completed;
+        Registered?Completed -> End;
+    }
+
+    let msg: Option<ClientCallbackMsgs> = None;
+    match msg {
+        Some(ClientCallbackMsgs::Register(msg)) => assert_type!(msg, Register),
+        Some(ClientCallbackMsgs::Completed(msg)) => assert_type!(msg, Completed),
+        _ => (),
+    }
+
+    struct UnregisteredState;
+
+    impl Unregistered for UnregisteredState {
+        fn actor_impl() -> String {
+            "client".into()
+        }
+
+        type OnSendRegisterRegistered = RegisteredState;
+        type OnSendRegisterFut = Ready<TransResult<Self, Self::OnSendRegisterRegistered>>;
+        fn on_send_register(self, _arg: &mut Register) -> Self::OnSendRegisterFut {
+            ready(TransResult::Ok(RegisteredState))
+        }
+    }
+
+    struct RegisteredState;
+
+    impl Registered for RegisteredState {
+        type HandleCompletedFut = Ready<TransResult<Self, End>>;
+        fn handle_completed(self, _arg: Completed) -> Self::HandleCompletedFut {
+            ready(TransResult::Ok(End))
+        }
+    }
+
+    struct ListeningState;
+
+    impl Listening for ListeningState {
+        fn actor_impl() -> String {
+            "server".into()
+        }
+
+        type HandleRegisterListening = ListeningState;
+        type HandleRegisterWorking = WorkingState;
+        type HandleRegisterFut = Ready<TransResult<Self, (ListeningState, WorkingState)>>;
+        fn handle_register(self, _arg: Register) -> Self::HandleRegisterFut {
+            ready(TransResult::Ok((self, WorkingState)))
+        }
+    }
+
+    struct WorkingState;
+
+    impl Working for WorkingState {
+        fn actor_impl() -> String {
+            "worker".into()
+        }
+
+        type OnSendCompletedFut = Ready<TransResult<Self, (End, Completed)>>;
+        fn on_send_completed(self) -> Self::OnSendCompletedFut {
+            ready(TransResult::Ok((End, Completed)))
+        }
+    }
+}

+ 1 - 0
crates/btrun/Cargo.toml

@@ -23,3 +23,4 @@ log = "0.4.17"
 btlib-tests = { path = "../btlib-tests" }
 env_logger = { version = "0.9.0" }
 ctor = { version = "0.1.22" }
+btproto = { path = "../btproto" }

+ 71 - 0
crates/btrun/src/kernel.rs

@@ -0,0 +1,71 @@
+//! This module contains the code necessary to run and supervise actors.
+
+use std::{future::Future, pin::Pin};
+
+use log::{debug, error};
+use tokio::{
+    select,
+    sync::{mpsc, oneshot},
+    task::{AbortHandle, JoinSet},
+};
+
+use crate::ActorPanic;
+
+pub(super) type BoxedFuture = Pin<Box<dyn Send + Future<Output = ()>>>;
+
+pub(super) struct SpawnReq {
+    task: BoxedFuture,
+    sender: oneshot::Sender<AbortHandle>,
+}
+
+impl SpawnReq {
+    pub(super) fn new<Fut>(task: Fut) -> (Self, oneshot::Receiver<AbortHandle>)
+    where
+        Fut: 'static + Send + Future<Output = ()>,
+    {
+        let (sender, receiver) = oneshot::channel();
+        let req = Self {
+            task: Box::pin(task),
+            sender,
+        };
+        (req, receiver)
+    }
+}
+
+/// The kernel is responsible for spawning and supervising actors.
+pub(super) async fn kernel(mut tasks: mpsc::Receiver<SpawnReq>) {
+    let mut running = JoinSet::<()>::new();
+
+    loop {
+        select! {
+            option = tasks.recv() => {
+                match option {
+                    Some(SpawnReq { task, sender }) => {
+                        let handle = running.spawn(task);
+                        if sender.send(handle).is_err() {
+                            error!("SpawnReq sender dropped the channel. Aborting actor.");
+                        }
+                    }
+                    None => panic!("The Runtime was dropped!")
+                };
+            }
+            Some(Err(join_error)) = running.join_next() => {
+                match join_error.try_into_panic() {
+                    Ok(panic_obj) => {
+                        if let Some(actor_panic) = panic_obj.downcast_ref::<ActorPanic>() {
+                            error!("{actor_panic}")
+                        } else if let Some(message) = panic_obj.downcast_ref::<String>() {
+                            error!("Actor panicked in an unknown state: {message}");
+                        } else {
+                            error!("Actor panicked for an unknown reason.");
+                        }
+                    }
+                    Err(join_error) => {
+                        debug!("Actor was aborted: {join_error}");
+                    }
+                };
+
+            }
+        }
+    }
+}

+ 362 - 475
crates/btrun/src/lib.rs

@@ -2,7 +2,7 @@
 
 use std::{
     any::Any,
-    collections::HashMap,
+    collections::{hash_map, HashMap},
     fmt::Display,
     future::{ready, Future, Ready},
     marker::PhantomData,
@@ -13,14 +13,18 @@ use std::{
 };
 
 use btlib::{bterr, crypto::Creds, error::StringError, BlockPath, Result};
-use btserde::{field_helpers::smart_ptr, from_slice, to_vec, write_to};
+use btserde::{from_slice, to_vec, write_to};
 use bttp::{DeserCallback, MsgCallback, Receiver, Replier, Transmitter};
-use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use kernel::{kernel, SpawnReq};
+use serde::{Deserialize, Serialize};
 use tokio::{
     sync::{mpsc, oneshot, Mutex, RwLock},
-    task::JoinHandle,
+    task::AbortHandle,
 };
-use uuid::Uuid;
+
+mod kernel;
+pub mod model;
+use model::*;
 
 /// Declares a new [Runtime] which listens for messages at the given IP address and uses the given
 /// [Creds]. Runtimes are intended to be created once in a process's lifetime and continue running
@@ -29,10 +33,10 @@ use uuid::Uuid;
 macro_rules! declare_runtime {
     ($name:ident, $ip_addr:expr, $creds:expr) => {
         ::lazy_static::lazy_static! {
-            static ref $name: &'static Runtime = {
+            static ref $name: &'static $crate::Runtime = {
                 ::lazy_static::lazy_static! {
-                    static ref RUNTIME: Runtime =  Runtime::_new($creds).unwrap();
-                    static ref RECEIVER: Receiver = _new_receiver($ip_addr, $creds, &*RUNTIME);
+                    static ref RUNTIME: $crate::Runtime = $crate::Runtime::_new($creds).unwrap();
+                    static ref RECEIVER: ::bttp::Receiver = _new_receiver($ip_addr, $creds, &*RUNTIME);
                 }
                 // By dereferencing RECEIVER we ensure it is started.
                 let _ = &*RECEIVER;
@@ -52,6 +56,9 @@ where
     Receiver::new(ip_addr, creds, callback).unwrap()
 }
 
+/// Type used to implement an actor's mailbox.
+pub type Mailbox<T> = mpsc::Receiver<Envelope<T>>;
+
 /// An actor runtime.
 ///
 /// Actors can be activated by the runtime and execute autonomously until they return. Running
@@ -60,11 +67,17 @@ where
 /// be ready when the reply has been received.
 pub struct Runtime {
     path: Arc<BlockPath>,
-    handles: RwLock<HashMap<Uuid, ActorHandle>>,
+    handles: RwLock<HashMap<ActorId, ActorHandle>>,
     peers: RwLock<HashMap<Arc<BlockPath>, Transmitter>>,
+    registry: RwLock<HashMap<ServiceId, ServiceRecord>>,
+    kernel_sender: mpsc::Sender<SpawnReq>,
 }
 
 impl Runtime {
+    /// The size of the buffer to use for the channel between [Runtime] and [kernel] used for
+    /// spawning tasks.
+    const SPAWN_REQ_BUF_SZ: usize = 16;
+
     ///  This method is not intended to be called directly by downstream crates. Use the macro
     /// [declare_runtime] to create a [Runtime].
     ///
@@ -72,10 +85,14 @@ impl Runtime {
     #[doc(hidden)]
     pub fn _new<C: 'static + Send + Sync + Creds>(creds: Arc<C>) -> Result<Runtime> {
         let path = Arc::new(creds.bind_path()?);
+        let (sender, receiver) = mpsc::channel(Self::SPAWN_REQ_BUF_SZ);
+        tokio::task::spawn(kernel(receiver));
         Ok(Runtime {
             path,
             handles: RwLock::new(HashMap::new()),
             peers: RwLock::new(HashMap::new()),
+            registry: RwLock::new(HashMap::new()),
+            kernel_sender: sender,
         })
     }
 
@@ -96,16 +113,16 @@ impl Runtime {
         from: ActorName,
         msg: T,
     ) -> Result<()> {
-        if to.path == self.path {
+        if to.path().as_ref() == self.path.as_ref() {
             let guard = self.handles.read().await;
-            if let Some(handle) = guard.get(&to.act_id) {
+            if let Some(handle) = guard.get(&to.act_id()) {
                 handle.send(from, msg).await
             } else {
                 Err(bterr!("invalid actor name"))
             }
         } else {
             let guard = self.peers.read().await;
-            if let Some(peer) = guard.get(&to.path) {
+            if let Some(peer) = guard.get(to.path()) {
                 let buf = to_vec(&msg)?;
                 let wire_msg = WireMsg {
                     to,
@@ -114,10 +131,32 @@ impl Runtime {
                 };
                 peer.send(wire_msg).await
             } else {
-                // TODO: Use the filesystem to discover the address of the recipient and connect to
-                // it.
-                todo!()
+                todo!("Discover the network location of the recipient runtime and connect to it.")
+            }
+        }
+    }
+
+    /// Sends a message to the service identified by [ServiceName].
+    pub async fn send_service<T: 'static + SendMsg>(
+        &'static self,
+        to: ServiceAddr,
+        from: ActorName,
+        msg: T,
+    ) -> Result<()> {
+        if to.path().as_ref() == self.path.as_ref() {
+            let actor_id = self.service_provider(&to).await?;
+            let handles = self.handles.read().await;
+            if let Some(handle) = handles.get(&actor_id) {
+                handle.send(from, msg).await
+            } else {
+                panic!(
+                    "Service record '{}' had a non-existent actor with ID '{}'.",
+                    to.service_id(),
+                    actor_id
+                );
             }
+        } else {
+            todo!("Send the message to an appropriate peer.")
         }
     }
 
@@ -129,16 +168,16 @@ impl Runtime {
         from: ActorName,
         msg: T,
     ) -> Result<T::Reply> {
-        if to.path == self.path {
+        if to.path().as_ref() == self.path.as_ref() {
             let guard = self.handles.read().await;
-            if let Some(handle) = guard.get(&to.act_id) {
-                handle.call_through(from, msg).await
+            if let Some(handle) = guard.get(&to.act_id()) {
+                handle.call_through(msg).await
             } else {
                 Err(bterr!("invalid actor name"))
             }
         } else {
             let guard = self.peers.read().await;
-            if let Some(peer) = guard.get(&to.path) {
+            if let Some(peer) = guard.get(to.path()) {
                 let buf = to_vec(&msg)?;
                 let wire_msg = WireMsg {
                     to,
@@ -147,37 +186,89 @@ impl Runtime {
                 };
                 peer.call(wire_msg, ReplyCallback::<T>::new()).await?
             } else {
-                // TODO: Use the filesystem to discover the address of the recipient and connect to
-                // it.
-                todo!()
+                todo!("Use the filesystem to find the address of the recipient and connect to it.")
             }
         }
     }
 
-    /// Resolves the given [ServiceName] to an [ActorName] which is part of it.
-    pub async fn resolve<'a>(&'a self, _service: &ServiceName) -> Result<ActorName> {
-        todo!()
+    /// Calls a service identified by [ServiceName].
+    pub async fn call_service<T: 'static + CallMsg>(
+        &'static self,
+        to: ServiceAddr,
+        msg: T,
+    ) -> Result<T::Reply> {
+        if to.path().as_ref() == self.path.as_ref() {
+            let actor_id = self.service_provider(&to).await?;
+            let handles = self.handles.read().await;
+            if let Some(handle) = handles.get(&actor_id) {
+                handle.call_through(msg).await
+            } else {
+                panic!(
+                    "Service record '{}' had a non-existent actor with ID '{}'.",
+                    to.service_id(),
+                    actor_id
+                );
+            }
+        } else {
+            todo!("Send the message to an appropriate peer.")
+        }
+    }
+
+    fn service_not_registered_err(id: &ServiceId) -> btlib::Error {
+        bterr!("Service is not registered: '{id}'")
+    }
+
+    async fn service_provider(&'static self, to: &ServiceAddr) -> Result<ActorId> {
+        let actor_id = {
+            let registry = self.registry.read().await;
+            if let Some(record) = registry.get(to.service_id()) {
+                record.actor_ids.first().copied()
+            } else {
+                return Err(Self::service_not_registered_err(to.service_id()));
+            }
+        };
+        let actor_id = if let Some(actor_id) = actor_id {
+            actor_id
+        } else {
+            let mut registry = self.registry.write().await;
+            if let Some(record) = registry.get_mut(to.service_id()) {
+                // It's possible that another thread got the write lock before us and they
+                // already spawned an actor.
+                if record.actor_ids.is_empty() {
+                    let spawner = record.spawner.as_ref();
+                    let actor_name = spawner(self).await?;
+                    let actor_id = actor_name.act_id();
+                    record.actor_ids.push(actor_id);
+                    actor_id
+                } else {
+                    record.actor_ids[0]
+                }
+            } else {
+                return Err(Self::service_not_registered_err(to.service_id()));
+            }
+        };
+        Ok(actor_id)
     }
 
-    /// Activates a new actor using the given activator function and returns a handle to it.
-    pub async fn activate<Msg, F, Fut>(&'static self, activator: F) -> ActorName
+    /// Spawns a new actor using the given activator function and returns a handle to it.
+    pub async fn spawn<Msg, F, Fut>(&'static self, activator: F) -> ActorName
     where
         Msg: 'static + CallMsg,
         Fut: 'static + Send + Future<Output = ()>,
-        F: FnOnce(&'static Runtime, mpsc::Receiver<Envelope<Msg>>, Uuid) -> Fut,
+        F: FnOnce(&'static Runtime, Mailbox<Msg>, ActorId) -> Fut,
     {
         let mut guard = self.handles.write().await;
         let act_id = {
-            let mut act_id = Uuid::new_v4();
+            let mut act_id = ActorId::new();
             while guard.contains_key(&act_id) {
-                act_id = Uuid::new_v4();
+                act_id = ActorId::new();
             }
             act_id
         };
         let act_name = self.actor_name(act_id);
         let (tx, rx) = mpsc::channel::<Envelope<Msg>>(MAILBOX_LIMIT);
         // The deliverer closure is responsible for deserializing messages received over the wire
-        // and delivering them to the actor's mailbox, and sending replies to call messages.
+        // and delivering them to the actor's mailbox, as well as sending replies to call messages.
         let deliverer = {
             let buffer = Arc::new(Mutex::new(Vec::<u8>::new()));
             let tx = tx.clone();
@@ -191,7 +282,7 @@ impl Runtime {
                 let fut: FutureResult = Box::pin(async move {
                     let msg = result?;
                     if let Some(mut replier) = replier {
-                        let (envelope, rx) = Envelope::new_call(act_name, msg);
+                        let (envelope, rx) = Envelope::new_call(msg);
                         tx.send(envelope).await.map_err(|_| {
                             bterr!("failed to deliver message. Recipient may have halted.")
                         })?;
@@ -216,26 +307,76 @@ impl Runtime {
                 fut
             }
         };
-        let handle = tokio::task::spawn(activator(self, rx, act_id));
+        let (req, receiver) = SpawnReq::new(activator(self, rx, act_id));
+        self.kernel_sender
+            .send(req)
+            .await
+            .unwrap_or_else(|err| panic!("The kernel has panicked: {err}"));
+        let handle = receiver
+            .await
+            .unwrap_or_else(|err| panic!("Kernel failed to send abort handle: {err}"));
         let actor_handle = ActorHandle::new(handle, tx, deliverer);
         guard.insert(act_id, actor_handle);
         act_name
     }
 
-    /// Registers an actor as a service with the given [ServiceId].
-    pub async fn register<Msg, Fut, F, G>(
-        &self,
-        _id: ServiceId,
-        _activator: F,
-        _deserializer: G,
-    ) -> Result<()>
+    /// Registers a service activation closure for [ServiceId]. An error is returned if the
+    /// [ServiceId] has already been registered.
+    pub async fn register<Msg, F>(&self, id: ServiceId, spawner: F) -> Result<ServiceName>
     where
         Msg: 'static + CallMsg,
-        Fut: 'static + Send + Future<Output = ()>,
-        F: Fn(mpsc::Receiver<Envelope<Msg>>, Uuid) -> Fut,
-        G: 'static + Send + Sync + Fn(&[u8]) -> Result<Msg>,
+        F: 'static
+            + Send
+            + Sync
+            + Fn(&'static Runtime) -> Pin<Box<dyn Future<Output = Result<ActorName>>>>,
     {
-        todo!()
+        let mut guard = self.registry.write().await;
+        match guard.entry(id.clone()) {
+            hash_map::Entry::Vacant(entry) => {
+                entry.insert(ServiceRecord::new(spawner));
+                Ok(ServiceName::new(self.path().clone(), id.clone()))
+            }
+            hash_map::Entry::Occupied(_) => {
+                log::info!("Updated registration for service '{id}'.");
+                Ok(ServiceName::new(self.path().clone(), id))
+            }
+        }
+    }
+
+    /// Removes the registration for the service with the given ID.
+    ///
+    /// If a vector reference is given in `service_providers`, the service providers which
+    /// are part of the deregistered service are appended to it. Otherwise, their
+    /// handles are dropped and their tasks are aborted.
+    ///
+    /// A [RuntimeError::BadServiceId] error is returned if there is no service registration with
+    /// the given ID in this runtime.
+    pub async fn deregister(
+        &self,
+        id: &ServiceId,
+        service_providers: Option<&mut Vec<ActorHandle>>,
+    ) -> Result<()> {
+        let record = {
+            let mut registry = self.registry.write().await;
+            if let Some(record) = registry.remove(id) {
+                record
+            } else {
+                return Err(RuntimeError::BadServiceId(id.clone()).into());
+            }
+        };
+        let mut handles = self.handles.write().await;
+        let removed = record
+            .actor_ids
+            .into_iter()
+            .flat_map(|act_id| handles.remove(&act_id));
+        // If a vector was provided, we put all the removed service providers in it. Otherwise
+        // we just drop them.
+        if let Some(service_providers) = service_providers {
+            service_providers.extend(removed);
+        } else {
+            for _ in removed {}
+        }
+        Ok(())
     }
 
     /// Returns the [ActorHandle] for the actor with the given name.
@@ -247,9 +388,9 @@ impl Runtime {
     /// returned when the handle is dropped), and no further messages will be delivered to it by
     /// this runtime.
     pub async fn take(&self, name: &ActorName) -> Result<ActorHandle> {
-        if name.path == self.path {
+        if name.path().as_ref() == self.path.as_ref() {
             let mut guard = self.handles.write().await;
-            if let Some(handle) = guard.remove(&name.act_id) {
+            if let Some(handle) = guard.remove(&name.act_id()) {
                 Ok(handle)
             } else {
                 Err(RuntimeError::BadActorName(name.clone()).into())
@@ -260,7 +401,7 @@ impl Runtime {
     }
 
     /// Returns the name of the actor in this runtime with the given actor ID.
-    pub fn actor_name(&self, act_id: Uuid) -> ActorName {
+    pub fn actor_name(&self, act_id: ActorId) -> ActorName {
         ActorName::new(self.path.clone(), act_id)
     }
 }
@@ -271,34 +412,49 @@ impl Drop for Runtime {
     }
 }
 
+/// Closure type used to spawn new service providers.
+type Spawner =
+    Box<dyn Send + Sync + Fn(&'static Runtime) -> Pin<Box<dyn Future<Output = Result<ActorName>>>>>;
+
+struct ServiceRecord {
+    spawner: Spawner,
+    actor_ids: Vec<ActorId>,
+}
+
+impl ServiceRecord {
+    fn new<F>(spawner: F) -> Self
+    where
+        F: 'static
+            + Send
+            + Sync
+            + Fn(&'static Runtime) -> Pin<Box<dyn Future<Output = Result<ActorName>>>>,
+    {
+        Self {
+            spawner: Box::new(spawner),
+            actor_ids: Vec::new(),
+        }
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, Eq)]
 pub enum RuntimeError {
     BadActorName(ActorName),
+    BadServiceId(ServiceId),
 }
 
 impl Display for RuntimeError {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         match self {
             Self::BadActorName(name) => write!(f, "bad actor name: {name}"),
+            Self::BadServiceId(service_id) => {
+                write!(f, "service ID is not registered: {service_id}")
+            }
         }
     }
 }
 
 impl std::error::Error for RuntimeError {}
 
-#[allow(dead_code)]
-/// Represents the terminal state of an actor, where it stops processing messages and halts.
-struct End;
-
-#[allow(dead_code)]
-/// Delivered to an actor implementation when it starts up.
-pub struct Activate {
-    /// A reference to the `Runtime` which is running this actor.
-    rt: &'static Runtime,
-    /// The ID assigned to this actor.
-    act_id: Uuid,
-}
-
 /// Deserializes replies sent over the wire.
 struct ReplyCallback<T> {
     _phantom: PhantomData<T>,
@@ -312,6 +468,12 @@ impl<T: CallMsg> ReplyCallback<T> {
     }
 }
 
+impl<T: CallMsg> Default for ReplyCallback<T> {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
 impl<T: CallMsg> DeserCallback for ReplyCallback<T> {
     type Arg<'de> = WireReply<'de> where T: 'de;
     type Return = Result<T::Reply>;
@@ -365,7 +527,7 @@ impl RuntimeCallback {
 
     async fn deliver_local(&self, msg: WireMsg<'_>, replier: Option<Replier>) -> Result<()> {
         let guard = self.rt.handles.read().await;
-        if let Some(handle) = guard.get(&msg.to.act_id) {
+        if let Some(handle) = guard.get(&msg.to.act_id()) {
             let envelope = if let Some(replier) = replier {
                 WireEnvelope::Call { msg, replier }
             } else {
@@ -410,79 +572,6 @@ impl MsgCallback for RuntimeCallback {
     }
 }
 
-/// A unique identifier for a particular service.
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
-pub struct ServiceId(#[serde(with = "smart_ptr")] Arc<String>);
-
-impl From<String> for ServiceId {
-    fn from(value: String) -> Self {
-        Self(Arc::new(value))
-    }
-}
-
-impl<'a> From<&'a str> for ServiceId {
-    fn from(value: &'a str) -> Self {
-        Self(Arc::new(value.to_owned()))
-    }
-}
-
-/// A unique identifier for a service.
-///
-/// A service is a collection of actors in the same directory which provide some functionality.
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
-pub struct ServiceName {
-    /// The path to the directory containing the service.
-    #[serde(with = "smart_ptr")]
-    path: Arc<BlockPath>,
-    /// The id of the service.
-    service_id: ServiceId,
-}
-
-/// A unique identifier for a specific actor activation.
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
-pub struct ActorName {
-    /// The path to the directory containing this actor.
-    #[serde(with = "smart_ptr")]
-    path: Arc<BlockPath>,
-    /// A unique identifier for an actor activation. Even as an actor transitions to different types
-    /// as it handles messages, this value does not change. Thus this value can be used to trace an
-    /// actor through a series of state transitions.
-    act_id: Uuid,
-}
-
-impl ActorName {
-    pub fn new(path: Arc<BlockPath>, act_id: Uuid) -> Self {
-        Self { path, act_id }
-    }
-
-    pub fn path(&self) -> &Arc<BlockPath> {
-        &self.path
-    }
-
-    pub fn act_id(&self) -> Uuid {
-        self.act_id
-    }
-}
-
-impl Display for ActorName {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}@{}", self.act_id, self.path)
-    }
-}
-
-/// Trait for messages which expect exactly one reply.
-pub trait CallMsg: Serialize + DeserializeOwned + Send + Sync {
-    /// The reply type expected for this message.
-    type Reply: Serialize + DeserializeOwned + Send + Sync;
-}
-
-/// Trait for messages which expect exactly zero replies.
-pub trait SendMsg: CallMsg {}
-
-/// A type used to express when a reply is not expected for a message type.
-#[derive(Serialize, Deserialize)]
-enum NoReply {}
-
 /// The maximum number of messages which can be kept in an actor's mailbox.
 const MAILBOX_LIMIT: usize = 32;
 
@@ -494,6 +583,13 @@ struct WireMsg<'a> {
     payload: &'a [u8],
 }
 
+impl<'a> WireMsg<'a> {
+    #[allow(dead_code)]
+    fn new(to: ActorName, from: ActorName, payload: &'a [u8]) -> Self {
+        Self { to, from, payload }
+    }
+}
+
 impl<'a> bttp::CallMsg<'a> for WireMsg<'a> {
     type Reply<'r> = WireReply<'r>;
 }
@@ -521,42 +617,60 @@ impl<'de> WireEnvelope<'de> {
     }
 }
 
+pub enum EnvelopeKind<T: CallMsg> {
+    Call {
+        reply: Option<oneshot::Sender<T::Reply>>,
+    },
+    Send {
+        from: ActorName,
+    },
+}
+
+impl<T: CallMsg> EnvelopeKind<T> {
+    pub fn name(&self) -> &'static str {
+        match self {
+            Self::Call { .. } => "Call",
+            Self::Send { .. } => "Send",
+        }
+    }
+}
+
 /// Wrapper around a message type `T` which indicates who the message is from and, if the message
 /// was dispatched with `call`, provides a channel to reply to it.
 pub struct Envelope<T: CallMsg> {
-    from: ActorName,
-    reply: Option<oneshot::Sender<T::Reply>>,
     msg: T,
+    kind: EnvelopeKind<T>,
 }
 
 impl<T: CallMsg> Envelope<T> {
-    pub fn new(msg: T, reply: Option<oneshot::Sender<T::Reply>>, from: ActorName) -> Self {
-        Self { from, reply, msg }
+    pub fn new(msg: T, kind: EnvelopeKind<T>) -> Self {
+        Self { msg, kind }
     }
 
     /// Creates a new envelope containing the given message which does not expect a reply.
     fn new_send(from: ActorName, msg: T) -> Self {
         Self {
-            from,
+            kind: EnvelopeKind::Send { from },
             msg,
-            reply: None,
         }
     }
 
     /// Creates a new envelope containing the given message which expects exactly one reply.
-    fn new_call(from: ActorName, msg: T) -> (Self, oneshot::Receiver<T::Reply>) {
+    fn new_call(msg: T) -> (Self, oneshot::Receiver<T::Reply>) {
         let (tx, rx) = oneshot::channel::<T::Reply>();
         let envelope = Self {
-            from,
+            kind: EnvelopeKind::Call { reply: Some(tx) },
             msg,
-            reply: Some(tx),
         };
         (envelope, rx)
     }
 
     /// Returns the name of the actor which sent this message.
-    pub fn from(&self) -> &ActorName {
-        &self.from
+    pub fn from(&self) -> Option<&ActorName> {
+        match &self.kind {
+            EnvelopeKind::Send { from } => Some(from),
+            _ => None,
+        }
     }
 
     /// Returns a reference to the message in this envelope.
@@ -569,43 +683,44 @@ impl<T: CallMsg> Envelope<T> {
     /// If this message is not expecting a reply, or if this message has already been replied to,
     /// then an error is returned.
     pub fn reply(&mut self, reply: T::Reply) -> Result<()> {
-        if let Some(tx) = self.reply.take() {
-            if tx.send(reply).is_ok() {
-                Ok(())
-            } else {
-                Err(bterr!("failed to send reply"))
+        match &mut self.kind {
+            EnvelopeKind::Call { reply: tx } => {
+                if let Some(tx) = tx.take() {
+                    tx.send(reply).map_err(|_| bterr!("Failed to send reply."))
+                } else {
+                    Err(bterr!("Reply has already been sent."))
+                }
             }
-        } else {
-            Err(bterr!("reply already sent"))
+            _ => Err(bterr!("Can't reply to '{}' messages.", self.kind.name())),
         }
     }
 
     /// Returns true if this message expects a reply and it has not already been replied to.
     pub fn needs_reply(&self) -> bool {
-        self.reply.is_some()
+        matches!(&self.kind, EnvelopeKind::Call { .. })
     }
 
-    pub fn split(self) -> (T, Option<oneshot::Sender<T::Reply>>, ActorName) {
-        (self.msg, self.reply, self.from)
+    pub fn split(self) -> (T, EnvelopeKind<T>) {
+        (self.msg, self.kind)
     }
 }
 
 type FutureResult = Pin<Box<dyn Send + Future<Output = Result<()>>>>;
 
 pub struct ActorHandle {
-    handle: Option<JoinHandle<()>>,
+    handle: AbortHandle,
     sender: Box<dyn Send + Sync + Any>,
     deliverer: Box<dyn Send + Sync + Fn(WireEnvelope<'_>) -> FutureResult>,
 }
 
 impl ActorHandle {
-    fn new<T, F>(handle: JoinHandle<()>, sender: mpsc::Sender<Envelope<T>>, deliverer: F) -> Self
+    fn new<T, F>(handle: AbortHandle, sender: mpsc::Sender<Envelope<T>>, deliverer: F) -> Self
     where
         T: 'static + CallMsg,
         F: 'static + Send + Sync + Fn(WireEnvelope<'_>) -> FutureResult,
     {
         Self {
-            handle: Some(handle),
+            handle,
             sender: Box::new(sender),
             deliverer: Box::new(deliverer),
         }
@@ -614,7 +729,7 @@ impl ActorHandle {
     fn sender<T: 'static + CallMsg>(&self) -> Result<&mpsc::Sender<Envelope<T>>> {
         self.sender
             .downcast_ref::<mpsc::Sender<Envelope<T>>>()
-            .ok_or_else(|| bterr!("unexpected message type"))
+            .ok_or_else(|| bterr!("Attempt to send message as the wrong type."))
     }
 
     /// Sends a message to the actor represented by this handle.
@@ -627,13 +742,9 @@ impl ActorHandle {
         Ok(())
     }
 
-    pub async fn call_through<T: 'static + CallMsg>(
-        &self,
-        from: ActorName,
-        msg: T,
-    ) -> Result<T::Reply> {
+    pub async fn call_through<T: 'static + CallMsg>(&self, msg: T) -> Result<T::Reply> {
         let sender = self.sender()?;
-        let (envelope, rx) = Envelope::new_call(from, msg);
+        let (envelope, rx) = Envelope::new_call(msg);
         sender
             .send(envelope)
             .await
@@ -642,17 +753,8 @@ impl ActorHandle {
         Ok(reply)
     }
 
-    pub async fn returned(&mut self) -> Result<()> {
-        if let Some(handle) = self.handle.take() {
-            handle.await?;
-        }
-        Ok(())
-    }
-
-    pub fn abort(&mut self) {
-        if let Some(handle) = self.handle.take() {
-            handle.abort();
-        }
+    pub fn abort(&self) {
+        self.handle.abort();
     }
 }
 
@@ -662,59 +764,66 @@ impl Drop for ActorHandle {
     }
 }
 
+/// Sets up variable declarations and logging configuration to facilitate testing with a [Runtime].
+#[macro_export]
+macro_rules! test_setup {
+    () => {
+        const RUNTIME_ADDR: ::std::net::IpAddr =
+            ::std::net::IpAddr::V4(::std::net::Ipv4Addr::new(127, 0, 0, 1));
+        lazy_static! {
+            static ref RUNTIME_CREDS: ::std::sync::Arc<::btlib::crypto::ConcreteCreds> = {
+                let test_store = &::btlib_tests::TEST_STORE;
+                ::btlib::crypto::CredStore::node_creds(test_store).unwrap()
+            };
+        }
+        declare_runtime!(RUNTIME, RUNTIME_ADDR, RUNTIME_CREDS.clone());
+
+        lazy_static! {
+            /// A tokio async runtime.
+            ///
+            /// When the `#[tokio::test]` attribute is used on a test, a new current thread runtime
+            /// is created for each test
+            /// (source: https://docs.rs/tokio/latest/tokio/attr.test.html#current-thread-runtime).
+            /// This creates a problem, because the first test thread to access the `RUNTIME` static
+            /// will initialize its `Receiver` in its runtime, which will stop running at the end of
+            /// the test. Hence subsequent tests will not be able to send remote messages to this
+            /// `Runtime`.
+            ///
+            /// By creating a single async runtime which is used by all of the tests, we can avoid this
+            /// problem.
+            static ref ASYNC_RT: tokio::runtime::Runtime = ::tokio::runtime::Builder
+                ::new_current_thread()
+                .enable_all()
+                .build()
+                .unwrap();
+        }
+
+        /// The log level to use when running tests.
+        const LOG_LEVEL: &str = "warn";
+
+        #[::ctor::ctor]
+        #[allow(non_snake_case)]
+        fn ctor() {
+            ::std::env::set_var("RUST_LOG", format!("{},quinn=WARN", LOG_LEVEL));
+            let mut builder = ::env_logger::Builder::from_default_env();
+            ::btlib::log::BuilderExt::btformat(&mut builder).init();
+        }
+    };
+}
+
 #[cfg(test)]
-mod tests {
+pub mod test {
     use super::*;
 
-    use btlib::{
-        crypto::{ConcreteCreds, CredStore, CredsPriv},
-        log::BuilderExt,
-    };
+    use btlib::crypto::{CredStore, CredsPriv};
     use btlib_tests::TEST_STORE;
-    use btserde::to_vec;
     use bttp::BlockAddr;
-    use ctor::ctor;
     use lazy_static::lazy_static;
-    use std::{
-        net::{IpAddr, Ipv4Addr},
-        sync::atomic::{AtomicU8, Ordering},
-        time::{Duration, Instant},
-    };
-    use tokio::runtime::Builder;
+    use serde::{Deserialize, Serialize};
 
-    const RUNTIME_ADDR: IpAddr = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1));
-    lazy_static! {
-        static ref RUNTIME_CREDS: Arc<ConcreteCreds> = TEST_STORE.node_creds().unwrap();
-    }
-    declare_runtime!(RUNTIME, RUNTIME_ADDR, RUNTIME_CREDS.clone());
-
-    lazy_static! {
-        /// A tokio async runtime.
-        ///
-        /// When the `#[tokio::test]` attribute is used on a test, a new current thread runtime
-        /// is created for each test
-        /// (source: https://docs.rs/tokio/latest/tokio/attr.test.html#current-thread-runtime).
-        /// This creates a problem, because the first test thread to access the `RUNTIME` static
-        /// will initialize its `Receiver` in its runtime, which will stop running at the end of
-        /// the test. Hence subsequent tests will not be able to send remote messages to this
-        /// `Runtime`.
-        ///
-        /// By creating a single async runtime which is used by all of the tests, we can avoid this
-        /// problem.
-        static ref ASYNC_RT: tokio::runtime::Runtime = Builder::new_current_thread()
-            .enable_all()
-            .build()
-            .unwrap();
-    }
+    use crate::CallMsg;
 
-    /// The log level to use when running tests.
-    const LOG_LEVEL: &str = "warn";
-
-    #[ctor]
-    fn ctor() {
-        std::env::set_var("RUST_LOG", format!("{},quinn=WARN", LOG_LEVEL));
-        env_logger::Builder::from_default_env().btformat().init();
-    }
+    test_setup!();
 
     #[derive(Serialize, Deserialize)]
     struct EchoMsg(String);
@@ -726,14 +835,19 @@ mod tests {
     async fn echo(
         _rt: &'static Runtime,
         mut mailbox: mpsc::Receiver<Envelope<EchoMsg>>,
-        _act_id: Uuid,
+        _act_id: ActorId,
     ) {
         while let Some(envelope) = mailbox.recv().await {
-            let (msg, replier, ..) = envelope.split();
-            if let Some(replier) = replier {
-                if let Err(_) = replier.send(msg) {
-                    panic!("failed to send reply");
+            let (msg, kind) = envelope.split();
+            match kind {
+                EnvelopeKind::Call { reply } => {
+                    let replier =
+                        reply.unwrap_or_else(|| panic!("The reply has already been sent."));
+                    if let Err(_) = replier.send(msg) {
+                        panic!("failed to send reply");
+                    }
                 }
+                _ => panic!("Expected EchoMsg to be a Call Message."),
             }
         }
     }
@@ -742,8 +856,8 @@ mod tests {
     fn local_call() {
         ASYNC_RT.block_on(async {
             const EXPECTED: &str = "hello";
-            let name = RUNTIME.activate(echo).await;
-            let from = ActorName::new(name.path().clone(), Uuid::default());
+            let name = RUNTIME.spawn(echo).await;
+            let from = ActorName::new(name.path().clone(), ActorId::new());
 
             let reply = RUNTIME
                 .call(name.clone(), from, EchoMsg(EXPECTED.into()))
@@ -756,35 +870,6 @@ mod tests {
         })
     }
 
-    #[test]
-    fn remote_call() {
-        ASYNC_RT.block_on(async {
-            const EXPECTED: &str = "hello";
-            let actor_name = RUNTIME.activate(echo).await;
-            let bind_path = Arc::new(RUNTIME_CREDS.bind_path().unwrap());
-            let block_addr = Arc::new(BlockAddr::new(RUNTIME_ADDR, bind_path));
-            let transmitter = Transmitter::new(block_addr, RUNTIME_CREDS.clone())
-                .await
-                .unwrap();
-            let buf = to_vec(&EchoMsg(EXPECTED.to_string())).unwrap();
-            let wire_msg = WireMsg {
-                to: actor_name.clone(),
-                from: RUNTIME.actor_name(Uuid::default()),
-                payload: &buf,
-            };
-
-            let reply = transmitter
-                .call(wire_msg, ReplyCallback::<EchoMsg>::new())
-                .await
-                .unwrap()
-                .unwrap();
-
-            assert_eq!(EXPECTED, reply.0);
-
-            RUNTIME.take(&actor_name).await.unwrap();
-        });
-    }
-
     /// Tests the `num_running` method.
     ///
     /// This test uses its own runtime and so can use the `#[tokio::test]` attribute.
@@ -797,233 +882,35 @@ mod tests {
             TEST_STORE.node_creds().unwrap()
         );
         assert_eq!(0, LOCAL_RT.num_running().await);
-        let name = LOCAL_RT.activate(echo).await;
+        let name = LOCAL_RT.spawn(echo).await;
         assert_eq!(1, LOCAL_RT.num_running().await);
         LOCAL_RT.take(&name).await.unwrap();
         assert_eq!(0, LOCAL_RT.num_running().await);
     }
 
-    // The following code is a proof-of-concept for what types should be generated for a
-    // simple ping-pong protocol:
-    //
-    // protocol! {
-    //     ClientInit?Activate -> SentPing, Listening!Ping
-    //     ServerInit?Activate -> Listening
-    //     Listening?Ping -> End, SentPing!Ping::Reply
-    //     SentPing?Ping::Reply -> End
-    // }
-    //
-    // In words, the protocol is described as follows.
-    // 1. The ClientInit state receives the Activate message. It returns the SentPing state and a
-    //    Ping message to be sent to the Listening state.
-    // 2. The ServerInit state receives the Activate message. It returns the Listening state.
-    // 3. When the Listening state receives the Ping message it returns the End state and a
-    //    Ping::Reply message to be sent to the SentPing state.
-    // 4. When the SentPing state receives the Ping::Reply message it returns the End state.
-    //
-    // The End state represents an end to the session described by the protocol. When an actor
-    // transitions to the End state its function returns.
-    // The generated actor implementation is the sender of the Activate message.
-    // When a state is expecting a Reply message, an error occurs if the message is not received
-    // in a timely manner.
-
-    #[derive(Serialize, Deserialize)]
-    struct Ping;
-    impl CallMsg for Ping {
-        type Reply = PingReply;
-    }
-
-    // I was tempted to name this "Pong", but the proc macro wouldn't think to do that.
-    #[derive(Serialize, Deserialize)]
-    struct PingReply;
-
-    trait ClientInit {
-        type AfterActivate: SentPing;
-        type HandleActivateFut: Future<Output = Result<(Self::AfterActivate, Ping)>>;
-        fn handle_activate(self, msg: Activate) -> Self::HandleActivateFut;
-    }
-
-    trait ServerInit {
-        type AfterActivate: Listening;
-        type HandleActivateFut: Future<Output = Result<Self::AfterActivate>>;
-        fn handle_activate(self, msg: Activate) -> Self::HandleActivateFut;
-    }
-
-    trait Listening {
-        type HandlePingFut: Future<Output = Result<(End, PingReply)>>;
-        fn handle_ping(self, msg: Ping) -> Self::HandlePingFut;
-    }
-
-    trait SentPing {
-        type HandleReplyFut: Future<Output = Result<End>>;
-        fn handle_reply(self, msg: PingReply) -> Self::HandleReplyFut;
-    }
-
-    #[derive(Serialize, Deserialize)]
-    enum PingProtocolMsg {
-        Ping(Ping),
-        PingReply(PingReply),
-    }
-    impl CallMsg for PingProtocolMsg {
-        type Reply = PingProtocolMsg;
-    }
-    impl SendMsg for PingProtocolMsg {}
-
-    struct ClientInitState;
-
-    impl ClientInit for ClientInitState {
-        type AfterActivate = ClientState;
-        type HandleActivateFut = impl Future<Output = Result<(Self::AfterActivate, Ping)>>;
-        fn handle_activate(self, _msg: Activate) -> Self::HandleActivateFut {
-            ready(Ok((ClientState, Ping)))
-        }
-    }
-
-    struct ClientState;
-
-    impl SentPing for ClientState {
-        type HandleReplyFut = Ready<Result<End>>;
-        fn handle_reply(self, _msg: PingReply) -> Self::HandleReplyFut {
-            ready(Ok(End))
-        }
-    }
-
-    #[allow(dead_code)]
-    enum PingClientState {
-        Init(ClientInitState),
-        SentPing(ClientState),
-        End(End),
-    }
-
-    struct ServerInitState;
-
-    struct ServerState;
-
-    impl ServerInit for ServerInitState {
-        type AfterActivate = ServerState;
-        type HandleActivateFut = Ready<Result<Self::AfterActivate>>;
-        fn handle_activate(self, _msg: Activate) -> Self::HandleActivateFut {
-            ready(Ok(ServerState))
-        }
-    }
-
-    impl Listening for ServerState {
-        type HandlePingFut = impl Future<Output = Result<(End, PingReply)>>;
-        fn handle_ping(self, _msg: Ping) -> Self::HandlePingFut {
-            ready(Ok((End, PingReply)))
-        }
-    }
-
-    #[allow(dead_code)]
-    enum PingServerState {
-        ServerInit(ServerInitState),
-        Listening(ServerState),
-        End(End),
-    }
-
-    async fn ping_server(
-        counter: Arc<AtomicU8>,
-        rt: &'static Runtime,
-        mut mailbox: mpsc::Receiver<Envelope<PingProtocolMsg>>,
-        act_id: Uuid,
-    ) {
-        let mut state = {
-            let init = ServerInitState;
-            let state = init.handle_activate(Activate { rt, act_id }).await.unwrap();
-            PingServerState::Listening(state)
-        };
-        while let Some(envelope) = mailbox.recv().await {
-            let (msg, replier, _from) = envelope.split();
-            match (state, msg) {
-                (PingServerState::Listening(listening_state), PingProtocolMsg::Ping(msg)) => {
-                    let (new_state, reply) = listening_state.handle_ping(msg).await.unwrap();
-                    state = PingServerState::End(new_state);
-                    if let Err(_) = replier.unwrap().send(PingProtocolMsg::PingReply(reply)) {
-                        panic!("Failed to send Ping reply.");
-                    }
-                }
-                (_prev_state, _) => {
-                    panic!("Ping protocol violation.");
-                    // A real implementation should assign the previous state and log the error.
-                    // state = prev_state;
-                }
-            }
-            if let PingServerState::End(_) = state {
-                break;
-            }
-        }
-        counter.fetch_sub(1, Ordering::SeqCst);
-    }
-
-    async fn ping_client(
-        counter: Arc<AtomicU8>,
-        server_name: ActorName,
-        rt: &'static Runtime,
-        _mailbox: mpsc::Receiver<Envelope<PingProtocolMsg>>,
-        act_id: Uuid,
-    ) {
-        let init = ClientInitState;
-        let (state, msg) = init.handle_activate(Activate { rt, act_id }).await.unwrap();
-        let from = rt.actor_name(act_id);
-        let reply = rt
-            .call(server_name, from, PingProtocolMsg::Ping(msg))
-            .await
-            .unwrap();
-        if let PingProtocolMsg::PingReply(msg) = reply {
-            state.handle_reply(msg).await.unwrap();
-        } else {
-            panic!("Incorrect message type sent in reply to Ping.");
-        }
-        counter.fetch_sub(1, Ordering::SeqCst);
-    }
-
     #[test]
-    fn ping_pong_test() {
+    fn remote_call() {
         ASYNC_RT.block_on(async {
-            let counter = Arc::new(AtomicU8::new(2));
-            let server_name = {
-                let counter = counter.clone();
-                RUNTIME
-                    .activate(move |rt, mailbox, act_id| ping_server(counter, rt, mailbox, act_id))
-                    .await
-            };
-            let client_name = {
-                let server_name = server_name.clone();
-                let counter = counter.clone();
-                RUNTIME
-                    .activate(move |rt, mailbox, act_id| {
-                        ping_client(counter, server_name, rt, mailbox, act_id)
-                    })
-                    .await
-            };
+            const EXPECTED: &str = "hello";
+            let actor_name = RUNTIME.spawn(echo).await;
+            let bind_path = Arc::new(RUNTIME_CREDS.bind_path().unwrap());
+            let block_addr = Arc::new(BlockAddr::new(RUNTIME_ADDR, bind_path));
+            let transmitter = Transmitter::new(block_addr, RUNTIME_CREDS.clone())
+                .await
+                .unwrap();
+            let buf = to_vec(&EchoMsg(EXPECTED.to_string())).unwrap();
+            let wire_msg =
+                WireMsg::new(actor_name.clone(), RUNTIME.actor_name(ActorId::new()), &buf);
 
-            let deadline = Instant::now() + Duration::from_millis(500);
-            while counter.load(Ordering::SeqCst) > 0 && Instant::now() < deadline {
-                tokio::time::sleep(Duration::from_millis(20)).await;
-            }
-            // Check that both tasks finished successfully and we didn't just timeout.
-            assert_eq!(0, counter.load(Ordering::SeqCst));
+            let reply = transmitter
+                .call(wire_msg, ReplyCallback::<EchoMsg>::new())
+                .await
+                .unwrap()
+                .unwrap();
 
-            // TODO: Should actor which return be removed from the runtime automatically?
-            RUNTIME.take(&server_name).await.unwrap();
-            RUNTIME.take(&client_name).await.unwrap();
+            assert_eq!(EXPECTED, reply.0);
+
+            RUNTIME.take(&actor_name).await.unwrap();
         });
     }
-
-    // Here's another protocol example. This is the Customer and Travel Agency protocol used as an
-    // example in the survey paper "Behavioral Types in Programming Languages."
-    //
-    // protocol! {
-    //     CustomerInit?Activate -> Choosing
-    //     AgencyInit?Activate -> Listening
-    //     Choosing?Choice -> Choosing, Listening!Query|Accept|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
-    // }
-    //
-    // The Choice message is from the runtime itself. It represents receiving input from a user.
 }

+ 224 - 0
crates/btrun/src/model.rs

@@ -0,0 +1,224 @@
+//! This module contains types used to model the actor system implemented by the runtime.
+
+use std::{fmt::Display, sync::Arc};
+
+use btlib::BlockPath;
+use btserde::field_helpers::smart_ptr;
+use serde::{de::DeserializeOwned, Deserialize, Serialize};
+use uuid::Uuid;
+
+/// Represents the result of an actor state transition, which can be one of the following:
+/// * `Success`: The transition succeeded and the new state is contained in the result.
+/// * `Abort`: The transition failed and the previous state along with an error describing
+///   the failure is in the result.
+/// * `Fatal`: The transition failed and the actor cannot recover from this failure. An error
+///   describing the failure is contained in the result.
+pub enum TransResult<From, To> {
+    /// Represents a successful transition.
+    Ok(To),
+    /// Represents an aborted transition.
+    Abort { from: From, err: btlib::Error },
+    /// Represents a failed transition which kills the actor which attempted it.
+    Fatal { err: btlib::Error },
+}
+
+/// Specifies a kind of transition, either a `Send` or a `Receive`.
+pub enum TransKind {
+    /// A transition which doesn't receive any message, but sends one or more.
+    Send,
+    /// A transition which receives a message.
+    Receive,
+}
+
+impl TransKind {
+    const fn verb(&self) -> &'static str {
+        match self {
+            Self::Send => "sending",
+            Self::Receive => "receiving",
+        }
+    }
+}
+
+/// A struct which conveys information about where an actor panic occurred to the kernel.
+pub struct ActorPanic {
+    /// The name of the actor implementation which panicked.
+    pub actor_impl: String,
+    /// The name of the state the actor was in.
+    pub state: &'static str,
+    /// The name of the message the actor was handling, or the name of the first message it was
+    /// trying to send.
+    pub message: &'static str,
+    /// The kind of transition which was being attempted.
+    pub kind: TransKind,
+    /// An error describing why the panic occurred.
+    pub err: btlib::Error,
+}
+
+impl Display for ActorPanic {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "Actor {} panicked in state {} while {} a message of type {}: {}",
+            self.actor_impl,
+            self.state,
+            self.kind.verb(),
+            self.message,
+            self.err
+        )
+    }
+}
+
+/// Represents the terminal state of an actor, where it stops processing messages and halts.
+pub struct End;
+
+impl End {
+    /// Returns the identifier for this type which is expected in protocol definitions.
+    pub fn ident() -> &'static str {
+        stringify!(End)
+    }
+}
+
+/// A unique identifier for a particular service.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
+pub struct ServiceId(#[serde(with = "smart_ptr")] Arc<String>);
+
+impl ServiceId {
+    pub fn new(value: Arc<String>) -> Self {
+        Self(value)
+    }
+}
+
+impl AsRef<str> for ServiceId {
+    fn as_ref(&self) -> &str {
+        self.0.as_str()
+    }
+}
+
+impl<T: Into<String>> From<T> for ServiceId {
+    fn from(value: T) -> Self {
+        Self(Arc::new(value.into()))
+    }
+}
+
+impl Display for ServiceId {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(self.as_ref())
+    }
+}
+
+/// A unique identifier for a service.
+///
+/// A service is a collection of actors in the same directory which provide some functionality.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
+pub struct ServiceName {
+    /// The path to the directory containing the service.
+    #[serde(with = "smart_ptr")]
+    path: Arc<BlockPath>,
+    /// The id of the service.
+    service_id: ServiceId,
+}
+
+impl ServiceName {
+    pub fn new(path: Arc<BlockPath>, service_id: ServiceId) -> Self {
+        Self { path, service_id }
+    }
+}
+
+/// Indicates the set of service providers a message is addressed to.
+///
+/// The message could be delivered to any of the service providers in this set, at random.
+pub struct ServiceAddr {
+    /// The [ServiceName] to address the message to.
+    name: ServiceName,
+    #[allow(dead_code)]
+    /// Indicates if the message should be routed towards the root of the tree or away from it.
+    rootward: bool,
+}
+
+impl ServiceAddr {
+    pub fn new(name: ServiceName, rootward: bool) -> Self {
+        Self { name, rootward }
+    }
+
+    pub fn path(&self) -> &Arc<BlockPath> {
+        &self.name.path
+    }
+
+    pub fn service_id(&self) -> &ServiceId {
+        &self.name.service_id
+    }
+}
+
+/// An identifier for an actor which is unique in a given runtime.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
+pub struct ActorId(Uuid);
+
+impl ActorId {
+    pub fn new() -> Self {
+        Self(Uuid::new_v4())
+    }
+
+    /// Returns an actor ID that can't have messages delivered to it.
+    pub fn undeliverable() -> Self {
+        Self(Uuid::from_bytes([0; 16]))
+    }
+}
+
+impl Default for ActorId {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl Copy for ActorId {}
+
+impl Display for ActorId {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+/// A unique identifier for a specific actor activation.
+#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Serialize, Deserialize)]
+pub struct ActorName {
+    /// The path to the directory containing this actor.
+    #[serde(with = "smart_ptr")]
+    path: Arc<BlockPath>,
+    /// A unique identifier for an actor activation. Even as an actor transitions to different types
+    /// as it handles messages, this value does not change. Thus this value can be used to trace an
+    /// actor through a series of state transitions.
+    act_id: ActorId,
+}
+
+impl ActorName {
+    pub fn new(path: Arc<BlockPath>, act_id: ActorId) -> Self {
+        Self { path, act_id }
+    }
+
+    pub fn path(&self) -> &Arc<BlockPath> {
+        &self.path
+    }
+
+    pub fn act_id(&self) -> ActorId {
+        self.act_id
+    }
+}
+
+impl Display for ActorName {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}@{}", self.act_id, self.path)
+    }
+}
+
+/// Trait for messages which expect exactly one reply.
+pub trait CallMsg: Serialize + DeserializeOwned + Send + Sync {
+    /// The reply type expected for this message.
+    type Reply: Serialize + DeserializeOwned + Send + Sync;
+}
+
+/// Trait for messages which expect exactly zero replies.
+pub trait SendMsg: CallMsg {}
+
+/// A type used to express when a reply is not expected for a message type.
+#[derive(Serialize, Deserialize)]
+pub enum NoReply {}

+ 673 - 0
crates/btrun/tests/runtime_tests.rs

@@ -0,0 +1,673 @@
+#![feature(impl_trait_in_assoc_type)]
+
+use btrun::model::*;
+use btrun::test_setup;
+use btrun::*;
+
+use btlib::Result;
+use btproto::protocol;
+use lazy_static::lazy_static;
+use log;
+use serde::{Deserialize, Serialize};
+use std::{
+    future::{ready, Future, Ready},
+    sync::{
+        atomic::{AtomicU8, Ordering},
+        Arc,
+    },
+};
+
+test_setup!();
+
+mod ping_pong {
+    use super::*;
+
+    use btlib::bterr;
+
+    // The following code is a proof-of-concept for what types should be generated for a
+    // simple ping-pong protocol:
+    protocol! {
+        named PingProtocol;
+        let server = [Server];
+        let client = [Client];
+        Client -> End, >service(Server)!Ping;
+        Server?Ping -> End, >Client!Ping::Reply;
+    }
+    //
+    // In words, the protocol is described as follows.
+    // 1. When the Listening state receives the Ping message it returns the End state and a
+    //    Ping::Reply message to be sent to the SentPing state.
+    // 2. When the SentPing state receives the Ping::Reply message it returns the End state.
+    //
+    // The End state represents an end to the session described by the protocol. When an actor
+    // transitions to the End state its function returns.
+    // When a state is expecting a Reply message, an error occurs if the message is not received
+    // in a timely manner.
+
+    enum PingClientState<T: Client> {
+        Client(T),
+        End(End),
+    }
+
+    impl<T: Client> PingClientState<T> {
+        const fn name(&self) -> &'static str {
+            match self {
+                Self::Client(_) => "Client",
+                Self::End(_) => "End",
+            }
+        }
+    }
+
+    struct ClientHandle<T: Client> {
+        state: Option<PingClientState<T>>,
+        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, PingProtocolMsgs::Ping(msg))
+                            .await;
+                        (new_state, result)
+                    }
+                    TransResult::Abort { from, err } => {
+                        let new_state = PingClientState::Client(from);
+                        (new_state, Err(err))
+                    }
+                    TransResult::Fatal { err } => return Err(err),
+                },
+                state => {
+                    let result = Err(bterr!("Can't send Ping in state {}.", state.name()));
+                    (state, result)
+                }
+            };
+            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<T: Client>(init: T, runtime: &'static Runtime) -> ClientHandle<T> {
+        let state = Some(PingClientState::Client(init));
+        ClientHandle { state, runtime }
+    }
+
+    async fn register_server<Init, F>(
+        make_init: F,
+        rt: &'static Runtime,
+        id: ServiceId,
+    ) -> Result<ServiceName>
+    where
+        Init: 'static + Server,
+        F: 'static + Send + Sync + Clone + Fn() -> Init,
+    {
+        enum ServerState<S> {
+            Server(S),
+            End(End),
+        }
+
+        async fn server_loop<Init, F>(
+            _runtime: &'static Runtime,
+            make_init: F,
+            mut mailbox: Mailbox<PingProtocolMsgs>,
+            _act_id: ActorId,
+        ) where
+            Init: 'static + Server,
+            F: 'static + Send + Sync + FnOnce() -> Init,
+        {
+            let mut state = ServerState::Server(make_init());
+            while let Some(envelope) = mailbox.recv().await {
+                let (msg, msg_kind) = envelope.split();
+                state = match (state, msg) {
+                    (ServerState::Server(listening_state), PingProtocolMsgs::Ping(msg)) => {
+                        match listening_state.handle_ping(msg).await {
+                            TransResult::Ok((new_state, reply)) => match msg_kind {
+                                EnvelopeKind::Call { reply: replier } => {
+                                    let replier =
+                                        replier.expect("The reply has already been sent.");
+                                    if let Err(_) = replier.send(PingProtocolMsgs::PingReply(reply))
+                                    {
+                                        panic!("Failed to send Ping reply.");
+                                    }
+                                    ServerState::End(new_state)
+                                }
+                                _ => panic!("'Ping' was expected to be a Call message."),
+                            },
+                            TransResult::Abort { from, err } => {
+                                log::warn!("Aborted transition from the {} while handling the {} message: {}", "Server", "Ping", err);
+                                ServerState::Server(from)
+                            }
+                            TransResult::Fatal { err } => {
+                                panic!("Fatal error while handling Ping message in Server state: {err}");
+                            }
+                        }
+                    }
+                    (state, _) => state,
+                };
+
+                if let ServerState::End(_) = state {
+                    break;
+                }
+            }
+        }
+
+        rt.register::<PingProtocolMsgs, _>(id, move |runtime| {
+            let make_init = make_init.clone();
+            let fut = async move {
+                let actor_impl = runtime
+                    .spawn(move |_, mailbox, act_id| {
+                        server_loop(runtime, make_init, mailbox, act_id)
+                    })
+                    .await;
+                Ok(actor_impl)
+            };
+            Box::pin(fut)
+        })
+        .await
+    }
+
+    #[derive(Serialize, Deserialize)]
+    pub struct Ping;
+    impl CallMsg for Ping {
+        type Reply = PingReply;
+    }
+
+    #[derive(Serialize, Deserialize)]
+    pub struct PingReply;
+
+    struct ClientState {
+        counter: Arc<AtomicU8>,
+    }
+
+    impl ClientState {
+        fn new(counter: Arc<AtomicU8>) -> Self {
+            counter.fetch_add(1, Ordering::SeqCst);
+            Self { counter }
+        }
+    }
+
+    impl Client for ClientState {
+        fn actor_impl() -> String {
+            "client".into()
+        }
+
+        type OnSendPingFut = impl Future<Output = TransResult<Self, (End, PingReply)>>;
+        fn on_send_ping(self, _msg: &mut Ping) -> Self::OnSendPingFut {
+            self.counter.fetch_sub(1, Ordering::SeqCst);
+            ready(TransResult::Ok((End, PingReply)))
+        }
+    }
+
+    struct ServerState {
+        counter: Arc<AtomicU8>,
+    }
+
+    impl ServerState {
+        fn new(counter: Arc<AtomicU8>) -> Self {
+            counter.fetch_add(1, Ordering::SeqCst);
+            Self { counter }
+        }
+    }
+
+    impl Server for ServerState {
+        fn actor_impl() -> String {
+            "server".into()
+        }
+
+        type HandlePingFut = impl Future<Output = TransResult<Self, (End, PingReply)>>;
+        fn handle_ping(self, _msg: Ping) -> Self::HandlePingFut {
+            self.counter.fetch_sub(1, Ordering::SeqCst);
+            ready(TransResult::Ok((End, PingReply)))
+        }
+    }
+
+    #[test]
+    fn ping_pong_test() {
+        ASYNC_RT.block_on(async {
+            const SERVICE_ID: &str = "PingPongProtocolServer";
+            let service_id = ServiceId::from(SERVICE_ID);
+            let counter = Arc::new(AtomicU8::new(0));
+            let service_name = {
+                let service_counter = counter.clone();
+                let make_init = move || {
+                    let server_counter = service_counter.clone();
+                    ServerState::new(server_counter)
+                };
+                register_server(make_init, &RUNTIME, service_id.clone())
+                    .await
+                    .unwrap()
+            };
+            let mut client_handle = spawn_client(ClientState::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();
+        });
+    }
+}
+
+mod travel_agency {
+    use super::*;
+
+    // Here's another protocol example. This is the Customer and Travel Agency protocol used as an
+    // example in the survey paper "Behavioral Types in Programming Languages."
+    // Note that the Choosing state can send messages at any time, not just in response to another
+    // message because there is a transition from Choosing that doesn't use the receive operator
+    // (`?`).
+    protocol! {
+        named TravelAgency;
+        let agency = [Listening];
+        let customer = [Choosing];
+        Choosing -> Choosing, >service(Listening)!Query;
+        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)]
+    pub struct Query;
+
+    impl CallMsg for Query {
+        type Reply = ();
+    }
+
+    #[derive(Serialize, Deserialize)]
+    pub struct Reject;
+
+    impl CallMsg for Reject {
+        type Reply = ();
+    }
+
+    #[derive(Serialize, Deserialize)]
+    pub struct Accept;
+
+    impl CallMsg for Accept {
+        type Reply = ();
+    }
+}
+
+#[allow(dead_code)]
+mod client_callback {
+
+    use super::*;
+
+    use std::{panic::panic_any, time::Duration};
+    use tokio::{sync::oneshot, time::timeout};
+
+    #[derive(Serialize, Deserialize)]
+    pub struct Register {
+        factor: usize,
+    }
+
+    #[derive(Serialize, Deserialize)]
+    pub struct Completed {
+        value: usize,
+    }
+
+    protocol! {
+        named ClientCallback;
+        let server = [Listening];
+        let worker = [Working];
+        let client = [Unregistered, Registered];
+        Unregistered -> Registered, >service(Listening)!Register[Registered];
+        Listening?Register[Registered] -> Listening, Working[Registered];
+        Working[Registered] -> End, >Registered!Completed;
+        Registered?Completed -> End;
+    }
+
+    struct UnregisteredState {
+        sender: oneshot::Sender<usize>,
+    }
+
+    impl Unregistered for UnregisteredState {
+        fn actor_impl() -> String {
+            "client".into()
+        }
+
+        type OnSendRegisterRegistered = RegisteredState;
+        type OnSendRegisterFut = Ready<TransResult<Self, Self::OnSendRegisterRegistered>>;
+        fn on_send_register(self, _arg: &mut Register) -> Self::OnSendRegisterFut {
+            ready(TransResult::Ok(RegisteredState {
+                sender: self.sender,
+            }))
+        }
+    }
+
+    struct RegisteredState {
+        sender: oneshot::Sender<usize>,
+    }
+
+    impl Registered for RegisteredState {
+        type HandleCompletedFut = Ready<TransResult<Self, End>>;
+        fn handle_completed(self, arg: Completed) -> Self::HandleCompletedFut {
+            self.sender.send(arg.value).unwrap();
+            ready(TransResult::Ok(End))
+        }
+    }
+
+    struct ListeningState {
+        multiple: usize,
+    }
+
+    impl Listening for ListeningState {
+        fn actor_impl() -> String {
+            "server".into()
+        }
+
+        type HandleRegisterListening = ListeningState;
+        type HandleRegisterWorking = WorkingState;
+        type HandleRegisterFut = Ready<TransResult<Self, (ListeningState, WorkingState)>>;
+        fn handle_register(self, arg: Register) -> Self::HandleRegisterFut {
+            let multiple = self.multiple;
+            ready(TransResult::Ok((
+                self,
+                WorkingState {
+                    factor: arg.factor,
+                    multiple,
+                },
+            )))
+        }
+    }
+
+    struct WorkingState {
+        factor: usize,
+        multiple: usize,
+    }
+
+    impl Working for WorkingState {
+        fn actor_impl() -> String {
+            "worker".into()
+        }
+
+        type OnSendCompletedFut = Ready<TransResult<Self, (End, Completed)>>;
+        fn on_send_completed(self) -> Self::OnSendCompletedFut {
+            let value = self.multiple * self.factor;
+            ready(TransResult::Ok((End, Completed { value })))
+        }
+    }
+
+    use ::tokio::sync::Mutex;
+
+    enum ClientState<Init: Unregistered> {
+        Unregistered(Init),
+        Registered(Init::OnSendRegisterRegistered),
+        End(End),
+    }
+
+    impl<Init: Unregistered> ClientState<Init> {
+        pub fn name(&self) -> &'static str {
+            match self {
+                Self::Unregistered(_) => "Unregistered",
+                Self::Registered(_) => "Registered",
+                Self::End(_) => "End",
+            }
+        }
+    }
+
+    struct ClientHandle<Init: Unregistered> {
+        runtime: &'static Runtime,
+        state: Arc<Mutex<Option<ClientState<Init>>>>,
+        name: ActorName,
+    }
+
+    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);
+                    }
+                },
+                state => state,
+            };
+            *guard = Some(new_state);
+            Ok(())
+        }
+    }
+
+    async fn spawn_client<Init>(init: Init, runtime: &'static Runtime) -> ClientHandle<Init>
+    where
+        Init: 'static + Unregistered,
+    {
+        let state = Arc::new(Mutex::new(Some(ClientState::Unregistered(init))));
+        let name = {
+            let state = state.clone();
+            runtime.spawn(move |_, mut mailbox, _act_id| async move {
+                while let Some(envelope) = mailbox.recv().await {
+                    let mut guard = state.lock().await;
+                    let state = guard.take()
+                        .unwrap_or_else(|| panic!("Logic error. The state was not returned."));
+                    let (msg, _kind) = envelope.split();
+                    let new_state = match (state, msg) {
+                        (ClientState::Registered(curr_state), ClientCallbackMsgs::Completed(msg)) => {
+                            match curr_state.handle_completed(msg).await {
+                                TransResult::Ok(next) =>  ClientState::<Init>::End(next),
+                                TransResult::Abort { from, err } => {
+                                    log::warn!("Aborted transition from the {} state while handling the {} message: {}", "Registered", "Completed", err);
+                                    ClientState::Registered(from)
+                                }
+                                TransResult::Fatal { err } => {
+                                    panic_any(ActorPanic {
+                                        actor_impl: Init::actor_impl(),
+                                        state: "Registered",
+                                        message: "Completed",
+                                        kind: TransKind::Receive,
+                                        err
+                                    });
+                                }
+                            }
+                        }
+                        (state, msg) => {
+                            log::error!("Unexpected message {} in state {}.", msg.name(), state.name());
+                            state
+                        }
+                    };
+                    *guard = Some(new_state);
+                }
+            }).await
+        };
+        ClientHandle {
+            runtime,
+            state,
+            name,
+        }
+    }
+
+    async fn register_server<Init, F>(
+        make_init: F,
+        runtime: &'static Runtime,
+        service_id: ServiceId,
+    ) -> Result<ServiceName>
+    where
+        Init: 'static + Listening<HandleRegisterListening = Init>,
+        F: 'static + Send + Sync + Clone + Fn() -> Init,
+    {
+        enum ServerState<S: Listening> {
+            Listening(S),
+        }
+
+        impl<S: Listening> ServerState<S> {
+            fn name(&self) -> &'static str {
+                match self {
+                    Self::Listening(_) => "Listening",
+                }
+            }
+        }
+
+        async fn server_loop<Init, F>(
+            runtime: &'static Runtime,
+            make_init: F,
+            mut mailbox: Mailbox<ClientCallbackMsgs>,
+            _act_id: ActorId,
+        ) where
+            Init: 'static + Listening<HandleRegisterListening = Init>,
+            F: 'static + Send + Sync + Fn() -> Init,
+        {
+            let mut state = ServerState::Listening(make_init());
+            while let Some(envelope) = mailbox.recv().await {
+                let (msg, msg_kind) = envelope.split();
+                let new_state = match (state, msg) {
+                    (ServerState::Listening(curr_state), ClientCallbackMsgs::Register(msg)) => {
+                        match curr_state.handle_register(msg).await {
+                            TransResult::Ok((new_state, working_state)) => {
+                                if let EnvelopeKind::Send { from, .. } = msg_kind {
+                                    start_worker(working_state, from, runtime).await;
+                                } else {
+                                    log::error!("Expected Register to be a Send message.");
+                                }
+                                ServerState::Listening(new_state)
+                            }
+                            TransResult::Abort { from, err } => {
+                                log::warn!("Aborted transition from the {} state while handling the {} message: {}", "Listening", "Register", err);
+                                ServerState::Listening(from)
+                            }
+                            TransResult::Fatal { err } => {
+                                panic_any(ActorPanic {
+                                    actor_impl: Init::actor_impl(),
+                                    state: "Listening",
+                                    message: "Register",
+                                    kind: TransKind::Receive,
+                                    err,
+                                });
+                            }
+                        }
+                    }
+                    (state, msg) => {
+                        log::error!(
+                            "Unexpected message {} in state {}.",
+                            msg.name(),
+                            state.name()
+                        );
+                        state
+                    }
+                };
+                state = new_state;
+            }
+        }
+
+        runtime
+            .register::<ClientCallbackMsgs, _>(service_id, move |runtime: &'static Runtime| {
+                let make_init = make_init.clone();
+                let fut = async move {
+                    let make_init = make_init.clone();
+                    let actor_impl = runtime
+                        .spawn(move |_, mailbox, act_id| {
+                            server_loop(runtime, make_init, mailbox, act_id)
+                        })
+                        .await;
+                    Ok(actor_impl)
+                };
+                Box::pin(fut)
+            })
+            .await
+    }
+
+    async fn start_worker<Init>(
+        init: Init,
+        owned: ActorName,
+        runtime: &'static Runtime,
+    ) -> ActorName
+    where
+        Init: 'static + Working,
+    {
+        enum WorkerState<S: Working> {
+            Working(S),
+        }
+
+        runtime
+            .spawn::<ClientCallbackMsgs, _, _>(move |_, _, act_id| async move {
+                let msg = match init.on_send_completed().await {
+                    TransResult::Ok((End, msg)) => msg,
+                    TransResult::Abort { err, .. } | TransResult::Fatal { err } => {
+                        panic_any(ActorPanic {
+                            actor_impl: Init::actor_impl(),
+                            state: "Working",
+                            message: "Completed",
+                            kind: TransKind::Send,
+                            err,
+                        })
+                    }
+                };
+                let from = runtime.actor_name(act_id);
+                let msg = ClientCallbackMsgs::Completed(msg);
+                runtime.send(owned, from, msg).await.unwrap_or_else(|err| {
+                    panic_any(ActorPanic {
+                        actor_impl: Init::actor_impl(),
+                        state: "Working",
+                        message: "Completed",
+                        kind: TransKind::Send,
+                        err,
+                    });
+                });
+            })
+            .await
+    }
+
+    #[test]
+    fn client_callback_protocol() {
+        ASYNC_RT.block_on(async {
+            const SERVICE_ID: &str = "ClientCallbackProtocolListening";
+            let service_id = ServiceId::from(SERVICE_ID);
+            let service_name = {
+                let make_init = move || ListeningState { multiple: 2 };
+                register_server(make_init, &RUNTIME, service_id.clone())
+                    .await
+                    .unwrap()
+            };
+            let (sender, receiver) = oneshot::channel();
+            let client_handle = spawn_client(UnregisteredState { sender }, &RUNTIME).await;
+            let service_addr = ServiceAddr::new(service_name, false);
+            client_handle
+                .send_register(service_addr, Register { factor: 21 })
+                .await
+                .unwrap();
+            let value = timeout(Duration::from_millis(500), receiver)
+                .await
+                .unwrap()
+                .unwrap();
+
+            assert_eq!(42, value);
+        });
+    }
+}

+ 12 - 0
crates/btsector/Cargo.toml

@@ -0,0 +1,12 @@
+[package]
+name = "btsector"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+btlib = { path = "../btlib" }
+btrun = { path = "../btrun" }
+btproto = { path = "../btproto" }
+serde = { version = "^1.0.136", features = ["derive"] }

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

@@ -0,0 +1,67 @@
+//! Types which define the protocol used by the sector layer.
+
+use btlib::{crypto::merkle_stream::VariantMerkleTree, BlockMeta, Inode};
+use btproto::protocol;
+use btrun::model::CallMsg;
+
+use serde::{Deserialize, Serialize};
+
+#[derive(Serialize, Deserialize)]
+pub struct SectorMsg {
+    id: SectorId,
+    op: SectorOperation,
+}
+
+impl CallMsg for SectorMsg {
+    type Reply = SectorMsgReply;
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct SectorId {
+    file: FileId,
+    sector: SectorKind,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct FileId {
+    generation: u64,
+    inode: Inode,
+}
+
+#[derive(Serialize, Deserialize)]
+pub enum SectorKind {
+    Meta,
+    Merkle,
+    Data(u64),
+}
+
+#[derive(Serialize, Deserialize)]
+pub enum SectorOperation {
+    Read,
+    Write(WriteOperation),
+}
+
+#[derive(Serialize, Deserialize)]
+pub enum WriteOperation {
+    Meta(Box<BlockMeta>),
+    Data {
+        meta: Box<BlockMeta>,
+        contents: Vec<u8>,
+    },
+}
+
+#[derive(Serialize, Deserialize)]
+pub enum SectorMsgReply {
+    Meta(BlockMeta),
+    Merkle(VariantMerkleTree),
+    Data(Vec<u8>),
+}
+
+protocol! {
+    named SectorProtocol;
+    let server = [Listening];
+    let client = [Client];
+    Client -> Client, >service(Listening)!SectorMsg;
+    Listening?SectorMsg -> Listening, >Client!SectorMsg::Reply;
+    Client?SectorMsg::Reply -> Client;
+}

+ 2 - 2
doc/BlocktreeDce/BlocktreeDce.tex

@@ -726,7 +726,7 @@ The byte offset in the plaintext of the file at which each data sector begins ca
 multiplying the sector's index by the sector size of the file.
 The \texttt{SectorId} type is used to identify a sector.
 \begin{verbatim}
-  pub enum SectorId {
+  pub struct SectorId {
     generation: u64,
     inode: u64,
     sector: SectorKind,
@@ -841,7 +841,7 @@ They use the credentials of the runtime they're hosted in to decrypt sector data
 information contained in file metadata.
 File actors are also responsible for encrypting and integrity protecting data written to files.
 In order for these actors to produce a signature over the root of the file's Merkle tree,
-it maintains a copy of the tree in memory.
+they each maintains a copy of it in memory.
 This copy is read from the sector service when the file is opened.
 While this does mean duplicating data between the sector and filesystem services,
 this design was chosen to reduce the network traffic between the two services,