فهرست منبع

Modified btproto validation to prevent the definition of
certain unobservable states.

Matthew Carr 1 سال پیش
والد
کامیت
b8f54c6aae

+ 13 - 5
crates/btfs/src/lib.rs

@@ -1,6 +1,6 @@
 use btlib::Result;
 use btproto::protocol;
-use btrun::{ActorName, CallMsg};
+use btrun::{ActorName, CallMsg, NoReply, SendMsg};
 use btsector::FileId;
 use serde::{Deserialize, Serialize};
 
@@ -12,6 +12,16 @@ pub struct Open {
 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];
@@ -23,15 +33,13 @@ protocol! {
     Listening?Query -> Listening, >Client!Query::Reply;
     Client?Query::Reply -> Client;
 
-    Client -> Client, >service(Listening)!Open;
-    Listening?Open -> Listening, FileInit, >Client!Open::Reply[Opened], FileInit!Open;
-    Client?Open::Reply[Opened] -> Client, FileHandle[Opened];
+    Client -> Client, FileHandle[FileInit], >service(Listening)!Open;
+    Listening?Open -> Listening, FileInit, >Client!Open::Reply[FileInit], FileInit!Open;
 
     FileInit?Open -> Opened;
 
     FileHandle[Opened] -> FileHandle[Opened], >Opened!FileOp;
     Opened?FileOp -> Opened, >FileHandle!FileOp::Reply;
-    FileHandle?FileOp::Reply -> FileHandle;
 
     FileHandle[Opened] -> End, >Opened!Close;
     Opened?Close -> End;

+ 12 - 11
crates/btproto/src/error.rs

@@ -74,18 +74,19 @@ pub(crate) mod msgs {
     /// 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_ERR: &str =
-        "A transition must send or receive a message.";
-    pub(crate) const UNDECLARED_STATE_ERR: &str = "State was not declared.";
-    pub(crate) const UNUSED_STATE_ERR: &str = "State was declared but never used.";
-    pub(crate) const UNMATCHED_SENDER_ERR: &str = "No receiver found for message type.";
-    pub(crate) const UNMATCHED_RECEIVER_ERR: &str = "No sender found for message type.";
-    pub(crate) const UNDELIVERABLE_ERR: &str =
+    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_ERR: &str =
+    pub(crate) const INVALID_REPLY: &str =
         "Replies can only be used in transitions which handle messages.";
-    pub(crate) const MULTIPLE_REPLIES_ERR: &str =
+    pub(crate) const MULTIPLE_REPLIES: &str =
         "Only a single reply can be sent in response to any message.";
-    pub(crate) const CLIENT_RECEIVED_NON_REPLY_ERR: &str =
-        "A client actor cannot receive a message which is not a reply.";
+    pub(crate) const CLIENT_RECEIVED_NON_REPLY: &str =
+        "This client actor receives a message which is not a reply.";
+    pub(crate) const UNOBSERVABLE_STATE: &str =
+        "This client state is not allowed because it only receives replies. Such a state cannot be observed.";
 }

+ 35 - 26
crates/btproto/src/model.rs

@@ -57,10 +57,6 @@ impl ProtocolModel {
         &self.msg_lookup
     }
 
-    pub(crate) fn actors(&self) -> &HashMap<Rc<Ident>, ActorModel> {
-        &self.actors
-    }
-
     pub(crate) fn actors_iter(&self) -> impl Iterator<Item = &ActorModel> {
         self.actors.values()
     }
@@ -206,7 +202,7 @@ impl MethodModel {
             } else {
                 return Err(syn::Error::new(
                     def.span(),
-                    error::msgs::NO_MSG_SENT_OR_RECEIVED_ERR,
+                    error::msgs::NO_MSG_SENT_OR_RECEIVED,
                 ));
             }
             for dest in dests {
@@ -429,15 +425,15 @@ impl MsgLookup {
                 let msg_name = &in_msg.msg_type;
                 let msg_info = messages
                     .entry(msg_name.clone())
-                    .or_insert_with(|| MsgInfo::empty(msg_name.clone()));
+                    .or_insert_with(|| MsgInfo::empty(in_msg.clone()));
                 msg_info.record_receiver(in_msg, in_state.clone());
             }
-            for out_msg in transition.out_msgs.as_ref().iter() {
-                let msg_name = &out_msg.msg.msg_type;
+            for dest in transition.out_msgs.as_ref().iter() {
+                let msg = &dest.msg;
                 let msg_info = messages
-                    .entry(msg_name.clone())
-                    .or_insert_with(|| MsgInfo::empty(msg_name.clone()));
-                msg_info.record_sender(&out_msg.msg, in_state.clone());
+                    .entry(msg.msg_type.clone())
+                    .or_insert_with(|| MsgInfo::empty(msg.clone()));
+                msg_info.record_sender(&dest.msg, in_state.clone());
             }
         }
         Self { messages }
@@ -468,12 +464,10 @@ impl AsRef<HashMap<Rc<Ident>, MsgInfo>> for MsgLookup {
     }
 }
 
+#[cfg_attr(test, derive(Debug))]
 pub(crate) struct MsgInfo {
-    /// The unique name of this message. If it is a reply, it will end in
-    /// `MessageReplyPart::REPLY_IDENT`.
+    def: Rc<Message>,
     msg_name: Rc<Ident>,
-    /// 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::CallMsg>::Reply`.
     msg_type: Rc<TokenStream>,
     is_reply: bool,
     senders: HashSet<Rc<Ident>>,
@@ -482,21 +476,19 @@ pub(crate) struct MsgInfo {
 }
 
 impl MsgInfo {
-    fn empty(msg_name: Rc<Ident>) -> Self {
+    fn empty(def: Rc<Message>) -> Self {
+        let msg_name = def.msg_type.as_ref();
         Self {
-            msg_name: msg_name.clone(),
+            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 is_call(&self) -> bool {
-        self.reply.is_some()
-    }
-
     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
@@ -511,10 +503,10 @@ impl MsgInfo {
         }
     }
 
-    fn info_for_mut(&mut self, msg: &Message) -> &mut 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.msg_type.clone());
+                let mut reply = MsgInfo::empty(msg.clone());
                 reply.is_reply = true;
                 reply.msg_name = Rc::new(format_ident!(
                     "{}{}",
@@ -530,20 +522,28 @@ impl MsgInfo {
         }
     }
 
-    fn record_receiver(&mut self, msg: &Message, receiver: Rc<Ident>) {
+    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: &Message, sender: Rc<Ident>) {
+    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::CallMsg>::Reply`.
     pub(crate) fn msg_type(&self) -> &Rc<TokenStream> {
         &self.msg_type
     }
@@ -551,6 +551,15 @@ impl MsgInfo {
     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 {
@@ -700,7 +709,7 @@ mod tests {
 
         let result = ProtocolModel::new(input);
 
-        assert_err(result, error::msgs::NO_MSG_SENT_OR_RECEIVED_ERR);
+        assert_err(result, error::msgs::NO_MSG_SENT_OR_RECEIVED);
     }
 
     #[test]

+ 13 - 8
crates/btproto/src/parsing.rs

@@ -319,7 +319,7 @@ impl AsRef<Punctuated<Rc<Ident>, Token![,]>> for IdentArray {
 #[cfg_attr(test, derive(Debug, PartialEq))]
 pub(crate) struct Transition {
     pub(crate) in_state: State,
-    in_msg: Option<(Token![?], Message)>,
+    in_msg: Option<(Token![?], Rc<Message>)>,
     arrow: Token![->],
     pub(crate) out_states: StatesList,
     redirect: Option<Token![>]>,
@@ -327,7 +327,7 @@ pub(crate) struct Transition {
 }
 
 impl Transition {
-    pub(crate) fn in_msg(&self) -> Option<&Message> {
+    pub(crate) fn in_msg(&self) -> Option<&Rc<Message>> {
         self.in_msg.as_ref().map(|(_, msg)| msg)
     }
 
@@ -352,7 +352,7 @@ impl Transition {
         };
         Self {
             in_state,
-            in_msg: in_msg.map(|msg| (Token![?](Span::call_site()), msg)),
+            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,
@@ -366,7 +366,7 @@ impl Parse for Transition {
     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, Message::parse(input)?))
+            Some((question_mark, Rc::new(Message::parse(input)?)))
         } else {
             None
         };
@@ -519,7 +519,7 @@ impl AsRef<Punctuated<Rc<Dest>, Token![,]>> for DestList {
 pub(crate) struct Dest {
     pub(crate) state: DestinationState,
     exclamation: Token![!],
-    pub(crate) msg: Message,
+    pub(crate) msg: Rc<Message>,
 }
 
 #[cfg(test)]
@@ -528,7 +528,7 @@ impl Dest {
         Self {
             state,
             exclamation: Token![!](Span::call_site()),
-            msg,
+            msg: Rc::new(msg),
         }
     }
 }
@@ -539,7 +539,7 @@ impl Parse for Dest {
         Ok(Self {
             state: input.parse()?,
             exclamation: input.parse()?,
-            msg: input.parse()?,
+            msg: Rc::new(input.parse()?),
         })
     }
 }
@@ -682,10 +682,15 @@ impl Parse for Message {
 
 impl GetSpan for Message {
     fn span(&self) -> Span {
+        let owned_states = if self.owned_states.as_ref().is_empty() {
+            None
+        } else {
+            Some(&self.owned_states)
+        };
         self.msg_type
             .span()
             .left_join(self.reply_part.as_ref())
-            .left_join(&self.owned_states)
+            .left_join(owned_states)
     }
 }
 

+ 231 - 47
crates/btproto/src/validation.rs

@@ -1,4 +1,4 @@
-use std::collections::HashSet;
+use std::{collections::HashSet, hash::Hash};
 
 use proc_macro2::{Ident, Span};
 
@@ -6,8 +6,8 @@ use btrun::End;
 
 use crate::{
     error::{self, MaybeErr},
-    model::ProtocolModel,
-    parsing::{DestinationState, GetSpan, Message, State},
+    model::{MsgInfo, ProtocolModel},
+    parsing::{DestinationState, GetSpan, State},
 };
 
 impl ProtocolModel {
@@ -18,6 +18,7 @@ impl ProtocolModel {
             .combine(self.no_undeliverable_msgs())
             .combine(self.replies_expected())
             .combine(self.clients_only_receive_replies())
+            .combine(self.no_unobservable_states())
             .into()
     }
 
@@ -67,12 +68,12 @@ impl ProtocolModel {
         }
         let undeclared: MaybeErr = used
             .difference(&declared)
-            .map(|ident| syn::Error::new(ident.span(), error::msgs::UNDECLARED_STATE_ERR))
+            .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_ERR))
+            .map(|ident| syn::Error::new(ident.span(), error::msgs::UNUSED_STATE))
             .collect();
         undeclared.combine(unused)
     }
@@ -80,29 +81,94 @@ impl ProtocolModel {
     /// 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(&self) -> MaybeErr {
-        let mut senders: HashSet<(&State, &Message)> = HashSet::new();
-        let mut receivers: HashSet<(&State, &Message)> = HashSet::new();
-        for transition in self.def().transitions.iter() {
-            if let Some(msg) = transition.in_msg() {
-                receivers.insert((&transition.in_state, msg));
+    fn receivers_and_senders_matched<'s>(&'s self) -> MaybeErr {
+        #[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 }
             }
-            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,
-                };
-                senders.insert((dest_state, &dest.msg));
+        }
+
+        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 extra_senders: MaybeErr = senders
-            .difference(&receivers)
-            .map(|pair| syn::Error::new(pair.1.msg_type.span(), error::msgs::UNMATCHED_SENDER_ERR))
+
+        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() {
+            for method in actor
+                .states()
+                .values()
+                .flat_map(|state| state.methods().values())
+            {
+                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 = receivers
-            .difference(&senders)
-            .map(|pair| {
-                syn::Error::new(pair.1.msg_type.span(), error::msgs::UNMATCHED_RECEIVER_ERR)
+        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)
@@ -142,7 +208,7 @@ impl ProtocolModel {
                             err = err.combine(
                                 syn::Error::new(
                                     dest_state.state_trait.span(),
-                                    error::msgs::UNDELIVERABLE_ERR,
+                                    error::msgs::UNDELIVERABLE,
                                 )
                                 .into(),
                             );
@@ -173,10 +239,7 @@ impl ProtocolModel {
                     replies
                         .iter()
                         .map(|reply| {
-                            syn::Error::new(
-                                reply.msg_type.span(),
-                                error::msgs::MULTIPLE_REPLIES_ERR,
-                            )
+                            syn::Error::new(reply.msg_type.span(), error::msgs::MULTIPLE_REPLIES)
                         })
                         .collect(),
                 );
@@ -186,7 +249,7 @@ impl ProtocolModel {
                     replies
                         .iter()
                         .map(|reply| {
-                            syn::Error::new(reply.msg_type.span(), error::msgs::INVALID_REPLY_ERR)
+                            syn::Error::new(reply.msg_type.span(), error::msgs::INVALID_REPLY)
                         })
                         .collect(),
                 );
@@ -198,8 +261,7 @@ impl ProtocolModel {
     /// A client is any actor with a state that sends at least one message when not handling an
     /// incoming message. Such actors are not allowed to receive any messages which are not replies.
     fn clients_only_receive_replies(&self) -> MaybeErr {
-        self.actors()
-            .values()
+        self.actors_iter()
             .filter(|actor| actor.is_client())
             .flat_map(|actor| {
                 actor
@@ -215,11 +277,27 @@ impl ProtocolModel {
                 }
             })
             .map(|transition| {
-                syn::Error::new(
-                    transition.span(),
-                    error::msgs::CLIENT_RECEIVED_NON_REPLY_ERR,
-                )
+                syn::Error::new(transition.span(), error::msgs::CLIENT_RECEIVED_NON_REPLY)
+            })
+            .collect()
+    }
+
+    /// 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()
     }
 }
@@ -229,7 +307,7 @@ mod tests {
     use super::*;
     use crate::{
         error::{assert_err, assert_ok},
-        parsing::{ActorDef, Dest, NameDef, Protocol, Transition},
+        parsing::{ActorDef, Dest, Message, NameDef, Protocol, Transition},
     };
 
     #[test]
@@ -277,7 +355,7 @@ mod tests {
 
         let result = input.all_states_declared_and_used();
 
-        assert_err(result, error::msgs::UNDECLARED_STATE_ERR);
+        assert_err(result, error::msgs::UNDECLARED_STATE);
     }
 
     #[test]
@@ -296,7 +374,7 @@ mod tests {
 
         let result = input.all_states_declared_and_used();
 
-        assert_err(result, error::msgs::UNDECLARED_STATE_ERR);
+        assert_err(result, error::msgs::UNDECLARED_STATE);
     }
 
     #[test]
@@ -315,7 +393,7 @@ mod tests {
 
         let result = input.all_states_declared_and_used();
 
-        assert_err(result, error::msgs::UNDECLARED_STATE_ERR);
+        assert_err(result, error::msgs::UNDECLARED_STATE);
     }
 
     #[test]
@@ -334,7 +412,7 @@ mod tests {
 
         let result = input.all_states_declared_and_used();
 
-        assert_err(result, error::msgs::UNUSED_STATE_ERR);
+        assert_err(result, error::msgs::UNUSED_STATE);
     }
 
     #[test]
@@ -346,6 +424,61 @@ mod tests {
         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(
@@ -365,7 +498,7 @@ mod tests {
 
         let result = input.receivers_and_senders_matched();
 
-        assert_err(result, error::msgs::UNMATCHED_SENDER_ERR);
+        assert_err(result, error::msgs::UNMATCHED_OUTGOING);
     }
 
     #[test]
@@ -384,7 +517,58 @@ mod tests {
 
         let result = input.receivers_and_senders_matched();
 
-        assert_err(result, error::msgs::UNMATCHED_RECEIVER_ERR);
+        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]
@@ -481,7 +665,7 @@ mod tests {
 
         let result = input.no_undeliverable_msgs();
 
-        assert_err(result, error::msgs::UNDELIVERABLE_ERR);
+        assert_err(result, error::msgs::UNDELIVERABLE);
     }
 
     #[test]
@@ -525,7 +709,7 @@ mod tests {
 
         let result = input.replies_expected();
 
-        assert_err(result, error::msgs::INVALID_REPLY_ERR);
+        assert_err(result, error::msgs::INVALID_REPLY);
     }
 
     #[test]
@@ -553,7 +737,7 @@ mod tests {
 
         let result = input.replies_expected();
 
-        assert_err(result, error::msgs::MULTIPLE_REPLIES_ERR);
+        assert_err(result, error::msgs::MULTIPLE_REPLIES);
     }
 
     #[test]
@@ -655,6 +839,6 @@ mod tests {
 
         let result = input.clients_only_receive_replies();
 
-        assert_err(result, error::msgs::CLIENT_RECEIVED_NON_REPLY_ERR);
+        assert_err(result, error::msgs::CLIENT_RECEIVED_NON_REPLY);
     }
 }

+ 6 - 17
crates/btproto/tests/protocol_tests.rs

@@ -61,10 +61,9 @@ fn reply() {
     protocol! {
         named ReplyTest;
         let server = [Listening];
-        let client = [Client, Waiting];
-        Client -> Waiting, >service(Listening)!Ping;
-        Listening?Ping -> Listening, >Waiting!Ping::Reply;
-        Waiting?Ping::Reply -> End;
+        let client = [Client];
+        Client -> Client, >service(Listening)!Ping;
+        Listening?Ping -> Listening, >Client!Ping::Reply;
     }
 
     let msg: Option<ReplyTestMsgs> = None;
@@ -87,20 +86,10 @@ fn reply() {
     struct ClientState;
 
     impl Client for ClientState {
-        type SendPingWaiting = WaitingState;
-        type SendPingFut = Ready<Result<(WaitingState, <Ping as CallMsg>::Reply)>>;
+        type SendPingClient = Self;
+        type SendPingFut = Ready<Result<(Self, <Ping as CallMsg>::Reply)>>;
         fn send_ping(self, _ping: Ping) -> Self::SendPingFut {
-            ready(Ok((WaitingState, ())))
-        }
-    }
-
-    struct WaitingState;
-
-    // TODO: This state should not be generated, as it is never observed.
-    impl Waiting for WaitingState {
-        type HandlePingReplyFut = Ready<Result<End>>;
-        fn handle_ping_reply(self, _msg: ()) -> Self::HandlePingReplyFut {
-            ready(Ok(End))
+            ready(Ok((self, ())))
         }
     }
 }

+ 1 - 1
crates/btrun/src/lib.rs

@@ -499,7 +499,7 @@ pub trait SendMsg: CallMsg {}
 
 /// A type used to express when a reply is not expected for a message type.
 #[derive(Serialize, Deserialize)]
-enum NoReply {}
+pub enum NoReply {}
 
 /// The maximum number of messages which can be kept in an actor's mailbox.
 const MAILBOX_LIMIT: usize = 32;

+ 3 - 4
crates/btrun/tests/runtime_tests.rs

@@ -155,10 +155,9 @@ mod ping_pong {
     protocol! {
         named PingPongProtocol;
         let server = [Server];
-        let client = [Client, Waiting];
-        Client -> Waiting, >service(Server)!Ping;
-        Server?Ping -> End, >Waiting!Ping::Reply;
-        Waiting?Ping::Reply -> End;
+        let client = [Client];
+        Client -> End, >service(Server)!Ping;
+        Server?Ping -> End, >Client!Ping::Reply;
     }
     //
     // In words, the protocol is described as follows.