Browse Source

Wrote tests for the btproto::parsing crate.

Matthew Carr 1 year ago
parent
commit
d900c9f263

+ 0 - 7
Cargo.lock

@@ -420,13 +420,6 @@ dependencies = [
  "syn 2.0.42",
 ]
 
-[[package]]
-name = "btproto-tests"
-version = "0.1.0"
-dependencies = [
- "btproto",
-]
-
 [[package]]
 name = "btprovision"
 version = "0.1.0"

+ 0 - 9
crates/btproto-tests/Cargo.toml

@@ -1,9 +0,0 @@
-[package]
-name = "btproto-tests"
-version = "0.1.0"
-edition = "2021"
-
-# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
-
-[dependencies]
-btproto = { path = "../btproto" }

+ 0 - 2
crates/btproto-tests/src/lib.rs

@@ -1,2 +0,0 @@
-#[cfg(test)]
-mod tests;

+ 0 - 58
crates/btproto-tests/src/tests.rs

@@ -1,58 +0,0 @@
-use btproto::protocol;
-
-mod ping_proto {
-    use super::*;
-
-    protocol! {
-        ClientInit?Activate -> SentPing, Listening!Ping
-        ServerInit?Activate -> Listening
-        Listening?Ping -> End, SentPing!Ping::Reply
-        SentPing?Ping::Reply -> End
-    }
-
-    // This protocol definition is observed by the procedural macro as the following sequence of
-    // tokens:
-    // Ident { ident: "ClientInit", span: #0 bytes(109..119) }
-    // Punct { ch: '?', spacing: Alone, span: #0 bytes(119..120) }
-    // Ident { ident: "Activate", span: #0 bytes(120..128) }
-    // Punct { ch: '-', spacing: Joint, span: #0 bytes(129..130) }
-    // Punct { ch: '>', spacing: Alone, span: #0 bytes(130..131) }
-    // Ident { ident: "SentPing", span: #0 bytes(132..140) }
-    // Punct { ch: ',', spacing: Alone, span: #0 bytes(140..141) }
-    // Ident { ident: "Listening", span: #0 bytes(142..151) }
-    // Punct { ch: '!', spacing: Alone, span: #0 bytes(151..152) }
-    // Ident { ident: "Ping", span: #0 bytes(152..156) }
-    // Ident { ident: "ServerInit", span: #0 bytes(165..175) }
-    // Punct { ch: '?', spacing: Alone, span: #0 bytes(175..176) }
-    // Ident { ident: "Activate", span: #0 bytes(176..184) }
-    // Punct { ch: '-', spacing: Joint, span: #0 bytes(185..186) }
-    // Punct { ch: '>', spacing: Alone, span: #0 bytes(186..187) }
-    // Ident { ident: "Listening", span: #0 bytes(188..197) }
-    // Ident { ident: "Listening", span: #0 bytes(206..215) }
-    // Punct { ch: '?', spacing: Alone, span: #0 bytes(215..216) }
-    // Ident { ident: "Ping", span: #0 bytes(216..220) }
-    // Punct { ch: '-', spacing: Joint, span: #0 bytes(221..222) }
-    // Punct { ch: '>', spacing: Alone, span: #0 bytes(222..223) }
-    // Ident { ident: "End", span: #0 bytes(224..227) }
-    // Punct { ch: ',', spacing: Alone, span: #0 bytes(227..228) }
-    // Ident { ident: "SentPing", span: #0 bytes(229..237) }
-    // Punct { ch: '!', spacing: Alone, span: #0 bytes(237..238) }
-    // Ident { ident: "Ping", span: #0 bytes(238..242) }
-    // Punct { ch: ':', spacing: Joint, span: #0 bytes(242..243) }
-    // Punct { ch: ':', spacing: Alone, span: #0 bytes(243..244) }
-    // Ident { ident: "Reply", span: #0 bytes(244..249) }
-    // Ident { ident: "SentPing", span: #0 bytes(258..266) }
-    // Punct { ch: '?', spacing: Alone, span: #0 bytes(266..267) }
-    // Ident { ident: "Ping", span: #0 bytes(267..271) }
-    // Punct { ch: ':', spacing: Joint, span: #0 bytes(271..272) }
-    // Punct { ch: ':', spacing: Alone, span: #0 bytes(272..273) }
-    // Ident { ident: "Reply", span: #0 bytes(273..278) }
-    // Punct { ch: '-', spacing: Joint, span: #0 bytes(279..280) }
-    // Punct { ch: '>', spacing: Alone, span: #0 bytes(280..281) }
-    // Ident { ident: "End", span: #0 bytes(282..285) }
-
-    #[test]
-    fn traits_created() {
-        assert!(true);
-    }
-}

+ 1 - 1
crates/btproto/Cargo.toml

@@ -11,4 +11,4 @@ proc-macro = true
 [dependencies]
 proc-macro2 = "1.0.71"
 quote = "1.0.33"
-syn = "2.0.42"
+syn = { version = "2.0.42", features = ["extra-traits"] }

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

@@ -0,0 +1,20 @@
+use super::Protocol;
+use proc_macro2::TokenStream;
+
+impl Protocol {
+    pub(crate) fn generate(&self) -> TokenStream {
+        let mut stream = self.generate_message_enum();
+        stream.extend(self.generate_state_traits());
+        stream
+    }
+
+    fn generate_message_enum(&self) -> TokenStream {
+        // TODO: Generate message enum.
+        TokenStream::new()
+    }
+
+    fn generate_state_traits(&self) -> TokenStream {
+        // TODO: Generate state traits.
+        TokenStream::new()
+    }
+}

+ 18 - 276
crates/btproto/src/lib.rs

@@ -1,290 +1,32 @@
 extern crate proc_macro;
 use proc_macro::TokenStream;
-use proc_macro2::{Punct, Spacing, TokenTree};
-use syn::{
-    braced, bracketed, parenthesized,
-    parse::{
-        discouraged::{AnyDelimiter, Speculative},
-        Parse, ParseBuffer, ParseStream,
-    },
-    punctuated::Punctuated,
-    token::{self, Bracket, Group, Paren},
-    Ident, Token, parse_macro_input,
-};
+use syn::parse_macro_input;
+
+mod parsing;
+use parsing::Protocol;
+
+mod generation;
+mod validation;
 
 /// Generates types for the parties participating in a messaging protocol.
 /// The grammar recognized by this macro is given below in the dialect of Extended Backus-Naur Form
 /// recognized by the `llgen` tool:
 /// ```ebnf
-/// protocol : name states_def transition* ;
-/// name : "let" "name" '=' ident ';' ;
-/// states_def : "let" "states" '=' states_array ';' ;
-/// states_array : '[' ident ( ',' ident )* ','? ']' ;
+/// protocol : name_def states_def transition* ;
+/// name_def : "let" "name" '=' Ident ';' ;
+/// states_def : "let" "states" '=' ident_array ';' ;
+/// ident_array : '[' Ident ( ',' Ident )* ','? ']' ;
 /// transition : state ( '?' message )?  "->" states_list ( '>' dest_list )? ';' ;
-/// states_list : state ',' ( state ',' )* ;
-/// state : ident states_array? ;
-/// message : ident ( "::" "Reply" )? states_array? ;
+/// state : Ident ident_array? ;
+/// states_list : state ( ',' state )* ','? ;
 /// dest_list : dest ( ',' dest )* ;
 /// dest : dest_state '!' message
-/// dest_state : ( "service" '(' ident ')' ) | state ;
+/// 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);
-    // TODO: Validate input.
-    // TODO: Generate message enum.
-    // TODO: Generate state traits.
-    TokenStream::new()
-}
-
-struct Protocol {
-    name_def: NameDef,
-    states_def: StatesDef,
-    transitions: Punctuated<Transition, Token![;]>,
-}
-
-impl Parse for Protocol {
-    /// protocol : name states_def transition* ;
-    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
-        Ok(Protocol {
-            name_def: input.parse()?,
-            states_def: input.parse()?,
-            transitions: input.parse_terminated(Transition::parse, Token![;])?,
-        })
-    }
-}
-
-struct NameDef {
-    name: Ident,
-}
-
-impl Parse for NameDef {
-    /// name : "let" "name" '=' ident ';' ;
-    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
-        input.parse::<Token![let]>()?;
-        if input.parse::<Ident>()?.to_string() != "name" {
-            return Err(input.error("invalid name declaration identifier"));
-        }
-        input.parse::<Token![=]>()?;
-        let name = input.parse::<Ident>()?;
-        input.parse::<Token![;]>()?;
-        Ok(NameDef { name })
-    }
-}
-
-struct StatesArray(Punctuated<Ident, Token![,]>);
-
-impl Parse for StatesArray {
-    /// states_array : '[' ident ( ',' ident )* ','? ']' ;
-    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
-        let content;
-        let bracket_token = bracketed!(content in input);
-        let states = content.parse_terminated(Ident::parse, Token![,])?;
-        if states.len() == 0 {
-            return Err(syn::Error::new(
-                bracket_token.span.open(),
-                "at least one state is required",
-            ));
-        }
-        Ok(Self(states))
-    }
-}
-
-struct StatesDef {
-    states_array: StatesArray,
-}
-
-impl Parse for StatesDef {
-    /// states_def : "let" "states" '=' states_array ';' ;
-    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
-        input.parse::<Token![let]>()?;
-        if input.parse::<Ident>()?.to_string() != "states" {
-            return Err(input.error("invalid states array identifier"));
-        };
-        input.parse::<Token![=]>()?;
-        let states_array = input.parse::<StatesArray>()?;
-        input.parse::<Token![;]>()?;
-        Ok(StatesDef { states_array })
-    }
-}
-
-struct Transition {
-    in_state: State,
-    in_msg: Option<Message>,
-    out_states: Punctuated<State, Token![,]>,
-    out_msgs: Option<Punctuated<Destination, Token![,]>>,
-}
-
-impl Parse for Transition {
-    /// transition : state ( '?' message )?  "->" states_list ( '>' dest_list )? ';' ;
-    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
-        let in_state = input.parse::<State>()?;
-        let in_msg = if let Ok(_) = input.parse::<Token![?]>() {
-            Some(input.parse::<Message>()?)
-        } else {
-            None
-        };
-        input.parse::<Token![->]>()?;
-        let mut out_states = Punctuated::<State, Token![,]>::new();
-        while !(input.peek(Token![>]) || input.peek(Token![;])) {
-            out_states.push_value(input.parse()?);
-            if let Ok(comma) = input.parse::<Token![,]>() {
-                out_states.push_punct(comma);
-            }
-        }
-        if out_states.len() == 0 {
-            return Err(input.error("at lest one out state is required"));
-        }
-        let out_msgs = if input.parse::<Token![>]>().is_ok() {
-            let mut out_msgs = Punctuated::<Destination, Token![,]>::new();
-            while !input.peek(Token![;]) {
-                out_msgs.push_value(input.parse()?);
-                if let Ok(comma) = input.parse::<Token![,]>() {
-                    out_msgs.push_puct(comma);
-                }
-            }
-            Some(out_msgs)
-        } else {
-            None
-        };
-        // Note that we must not eat the semicolon because the Punctuated parser expects it.
-        Ok(Self {
-            in_state,
-            in_msg,
-            out_states,
-            out_msgs,
-        })
-    }
-}
-
-struct Message {
-    msg_type: Ident,
-    is_reply: bool,
-    owned_states: StatesArray,
-}
-
-impl Parse for Message {
-    /// message : ident ( "::" "Reply" )? states_array? ;
-    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
-        let msg_type = input.parse::<Ident>()?;
-        let is_reply = input.peek(Token![::]);
-        if is_reply {
-            input.parse::<Token![::]>()?;
-            let reply = input.parse::<Ident>()?;
-            if reply.to_string() != "Reply" {
-                return Err(syn::Error::new(reply.span(), "expected 'Reply'"));
-            }
-        }
-        let owned_states = if input.peek(token::Bracket) {
-            input.parse::<StatesArray>()?
-        } else {
-            StatesArray(Punctuated::new())
-        };
-        Ok(Self {
-            msg_type,
-            is_reply,
-            owned_states,
-        })
-    }
-}
-
-struct State {
-    state_trait: Ident,
-    owned_states: Option<StatesArray>,
-}
-
-impl Parse for State {
-    /// state : ident states_array? ;
-    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
-        let state_trait = input.parse::<Ident>()?;
-        let owned_states = if input.peek(token::Bracket) {
-            Some(input.parse::<StatesArray>()?)
-        } else {
-            None
-        };
-        Ok(Self {
-            state_trait,
-            owned_states,
-        })
-    }
-}
-
-struct Destination {
-    state: DestinationState,
-    msg: Message,
-}
-
-impl Parse for Destination {
-    /// dest : dest_state '!' message
-    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
-        let state = input.parse::<DestinationState>()?;
-        input.parse::<Token![!]>()?;
-        let msg = input.parse::<Message>()?;
-        Ok(Self { state, msg })
-    }
-}
-
-struct DestinationState {
-    dest_state: Ident,
-    is_service: bool,
-}
-
-impl Parse for DestinationState {
-    /// dest_state : ( "service" '(' ident ')' ) | state ;
-    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
-        let ident = input.parse::<Ident>()?;
-        let is_service = ident.to_string() == "service";
-        if is_service {
-            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(),
-                "expected destination state",
-            ))?;
-            if let Some(extra_dest) = dest_states.next() {
-                return Err(syn::Error::new(
-                    extra_dest.span(),
-                    "only one destination state is allowed",
-                ));
-            }
-            Ok(DestinationState {
-                dest_state,
-                is_service,
-            })
-        } else {
-            Ok(DestinationState {
-                dest_state: ident,
-                is_service,
-            })
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn parse_destination_state_regular() {
-        const EXPECTED_DEST_STATE: &str = "Listening";
-
-        let actual = syn::parse_str::<DestinationState>(EXPECTED_DEST_STATE).unwrap();
-
-        assert_eq!(actual.dest_state.to_string(), EXPECTED_DEST_STATE);
-        assert!(!actual.is_service);
-    }
-
-    #[test]
-    fn parse_destination_state_service() {
-        const EXPECTED_DEST_STATE: &str = "Listening";
-        let input = format!("service({EXPECTED_DEST_STATE})");
-
-        let actual = syn::parse_str::<DestinationState>(&input).unwrap();
-
-        assert_eq!(actual.dest_state.to_string(), EXPECTED_DEST_STATE);
-        assert!(actual.is_service);
-    }
+    let input = parse_macro_input!(input as Protocol);
+    input.validate().unwrap();
+    TokenStream::from(input.generate())
 }

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

@@ -0,0 +1,837 @@
+//! Types for parsing the protocol grammar.
+
+use syn::{bracketed, parenthesized, parse::Parse, punctuated::Punctuated, token, Ident, 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,
+    pub(crate) states_def: StatesDef,
+    pub(crate) transitions: Punctuated<Transition, Token![;]>,
+}
+
+impl Parse for Protocol {
+    /// protocol : name_def states_def transition* ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        Ok(Protocol {
+            name_def: input.parse()?,
+            states_def: input.parse()?,
+            transitions: input.parse_terminated(Transition::parse, Token![;])?,
+        })
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct NameDef {
+    pub(crate) name: Ident,
+}
+
+impl NameDef {
+    const NAME_IDENT_ERR: &str = "invalid name declaration identifier";
+}
+
+impl Parse for NameDef {
+    /// name_def : "let" "name" '=' Ident ';' ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        input.parse::<Token![let]>()?;
+        if Ident::parse(input)? != "name" {
+            return Err(input.error(Self::NAME_IDENT_ERR));
+        }
+        input.parse::<Token![=]>()?;
+        let name = Ident::parse(input)?;
+        input.parse::<Token![;]>()?;
+        Ok(NameDef { name })
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct StatesDef {
+    pub(crate) states: IdentArray,
+}
+
+impl StatesDef {
+    const ARRAY_IDENT_ERR: &str = "invalid states array identifier";
+}
+
+impl Parse for StatesDef {
+    /// states_def : "let" "states" '=' ident_array ';' ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        input.parse::<Token![let]>()?;
+        if Ident::parse(input)? != "states" {
+            return Err(input.error(Self::ARRAY_IDENT_ERR));
+        };
+        input.parse::<Token![=]>()?;
+        let ident_array = IdentArray::parse(input)?;
+        input.parse::<Token![;]>()?;
+        Ok(StatesDef {
+            states: ident_array,
+        })
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct IdentArray(Punctuated<Ident, Token![,]>);
+
+impl IdentArray {
+    const EMPTY_ERR: &str = "at least one state is required";
+
+    fn empty() -> Self {
+        Self(Punctuated::new())
+    }
+}
+
+impl Parse for IdentArray {
+    /// ident_array : '[' Ident ( ',' Ident )* ','? ']' ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let content;
+        let bracket_token = bracketed!(content in input);
+        let states = content.parse_terminated(Ident::parse, Token![,])?;
+        if states.is_empty() {
+            return Err(syn::Error::new(bracket_token.span.open(), Self::EMPTY_ERR));
+        }
+        Ok(Self(states))
+    }
+}
+
+impl AsRef<Punctuated<Ident, Token![,]>> for IdentArray {
+    fn as_ref(&self) -> &Punctuated<Ident, Token![,]> {
+        &self.0
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct Transition {
+    pub(crate) in_state: State,
+    pub(crate) in_msg: Option<Message>,
+    pub(crate) out_states: StatesList,
+    pub(crate) out_msgs: DestList,
+}
+
+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 input.parse::<Token![?]>().is_ok() {
+            Some(Message::parse(input)?)
+        } else {
+            None
+        };
+        input.parse::<Token![->]>()?;
+        let out_states = StatesList::parse(input)?;
+        let out_msgs = if input.parse::<Token![>]>().is_ok() {
+            DestList::parse(input)?
+        } else {
+            DestList::empty()
+        };
+        // Note that we must not eat the semicolon because the Punctuated parser expects it.
+        Ok(Self {
+            in_state,
+            in_msg,
+            out_states,
+            out_msgs,
+        })
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct State {
+    pub(crate) state_trait: Ident,
+    pub(crate) owned_states: IdentArray,
+}
+
+impl Parse for State {
+    /// state : Ident ident_array? ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let state_trait = Ident::parse(input)?;
+        let owned_states = if input.peek(token::Bracket) {
+            IdentArray::parse(input)?
+        } else {
+            IdentArray::empty()
+        };
+        Ok(Self {
+            state_trait,
+            owned_states,
+        })
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct StatesList(Punctuated<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 mut states = Punctuated::new();
+        while !(input.peek(Token![>]) || input.peek(Token![;]) || input.cursor().eof()) {
+            states.push_value(input.parse()?);
+            if let Ok(comma) = input.parse::<Token![,]>() {
+                states.push_punct(comma);
+            }
+        }
+        if states.is_empty() {
+            return Err(input.error(Self::EMPTY_ERR));
+        }
+        Ok(Self(states))
+    }
+}
+
+impl AsRef<Punctuated<State, Token![,]>> for StatesList {
+    fn as_ref(&self) -> &Punctuated<State, Token![,]> {
+        &self.0
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct DestList(Punctuated<Dest, Token![,]>);
+
+impl DestList {
+    fn empty() -> Self {
+        Self(Punctuated::new())
+    }
+}
+
+impl Parse for DestList {
+    /// dest_list : dest ( ',' dest )* ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let mut dests = Punctuated::new();
+        while !(input.peek(Token![;]) || input.cursor().eof()) {
+            dests.push_value(input.parse()?);
+            if let Ok(comma) = input.parse::<Token![,]>() {
+                dests.push_punct(comma);
+            }
+        }
+        Ok(DestList(dests))
+    }
+}
+
+impl AsRef<Punctuated<Dest, Token![,]>> for DestList {
+    fn as_ref(&self) -> &Punctuated<Dest, Token![,]> {
+        &self.0
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct Dest {
+    pub(crate) state: DestinationState,
+    pub(crate) msg: Message,
+}
+
+impl Parse for Dest {
+    /// dest : dest_state '!' message
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let state = DestinationState::parse(input)?;
+        input.parse::<Token![!]>()?;
+        let msg = Message::parse(input)?;
+        Ok(Self { state, msg })
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) enum DestinationState {
+    Service(Ident),
+    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(dest_state))
+        } else {
+            Ok(DestinationState::Individual(input.parse()?))
+        }
+    }
+}
+
+#[cfg_attr(test, derive(Debug, PartialEq))]
+pub(crate) struct Message {
+    pub(crate) msg_type: Ident,
+    pub(crate) is_reply: bool,
+    pub(crate) owned_states: IdentArray,
+}
+
+impl Message {
+    const REPLY_ERR: &str = "expected 'Reply'";
+}
+
+impl Parse for Message {
+    /// message : Ident ( "::" "Reply" )? ident_array? ;
+    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
+        let msg_type = Ident::parse(input)?;
+        let is_reply = input.peek(Token![::]);
+        if is_reply {
+            input.parse::<Token![::]>()?;
+            let reply = Ident::parse(input)?;
+            if reply != "Reply" {
+                return Err(syn::Error::new(reply.span(), Self::REPLY_ERR));
+            }
+        }
+        let owned_states = if input.peek(token::Bracket) {
+            IdentArray::parse(input)?
+        } else {
+            IdentArray(Punctuated::new())
+        };
+        Ok(Self {
+            msg_type,
+            is_reply,
+            owned_states,
+        })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use proc_macro2::Span;
+    use std::iter::{self, IntoIterator};
+    use syn::parse_str;
+
+    fn ident(str: &str) -> Ident {
+        Ident::new(str, Span::call_site())
+    }
+
+    impl Protocol {
+        fn new(
+            name_def: NameDef,
+            states_def: StatesDef,
+            transitions: impl Iterator<Item = Transition>,
+        ) -> Self {
+            let mut transitions: Punctuated<Transition, Token![;]> = transitions.collect();
+            transitions.push_punct(Token![;](Span::call_site()));
+            Self {
+                name_def,
+                states_def,
+                transitions,
+            }
+        }
+    }
+
+    #[test]
+    fn protocol_parse_minimal() {
+        const EXPECTED_NAME: &str = "Foo";
+        const EXPECTED_STATES: [&str; 3] = ["First", "Second", "Third"];
+        let input = format!(
+            "let name = {EXPECTED_NAME};
+let states = [{}];
+{} -> {};
+{} -> {};",
+            EXPECTED_STATES.join(", "),
+            EXPECTED_STATES[0],
+            EXPECTED_STATES[1],
+            EXPECTED_STATES[1],
+            EXPECTED_STATES[2],
+        );
+        let expected = Protocol::new(
+            NameDef::new(EXPECTED_NAME),
+            StatesDef::new(EXPECTED_STATES.into_iter()),
+            [
+                Transition::new(
+                    State::new(EXPECTED_STATES[0], iter::empty()),
+                    None,
+                    [State::new(EXPECTED_STATES[1], iter::empty())].into_iter(),
+                    iter::empty(),
+                ),
+                Transition::new(
+                    State::new(EXPECTED_STATES[1], iter::empty()),
+                    None,
+                    [State::new(EXPECTED_STATES[2], iter::empty())].into_iter(),
+                    iter::empty(),
+                ),
+            ]
+            .into_iter(),
+        );
+
+        let actual = parse_str::<Protocol>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    impl NameDef {
+        fn new(name: &str) -> Self {
+            Self { name: ident(name) }
+        }
+    }
+
+    #[test]
+    fn name_def_parse() {
+        const EXPECTED_NAME: &str = "Foofercorg";
+        let input = format!("let name = {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>("let nam = Samson;");
+
+        assert!(result.is_err());
+        let err_str = result.err().unwrap().to_string();
+        assert_eq!(NameDef::NAME_IDENT_ERR, err_str);
+    }
+
+    impl StatesDef {
+        fn new(state_names: impl Iterator<Item = &'static str>) -> Self {
+            Self {
+                states: IdentArray::new(state_names),
+            }
+        }
+    }
+
+    #[test]
+    fn states_def_parse() {
+        const EXPECTED_STATES: [&str; 2] = ["First", "Second"];
+        let input = format!("let states = [{}];", EXPECTED_STATES.join(", "));
+        let expected = StatesDef::new(EXPECTED_STATES.into_iter());
+
+        let actual = parse_str::<StatesDef>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn states_def_parse_wrong_ident_err() {
+        let result = parse_str::<StatesDef>("let staties = [Alpha, Beta];");
+
+        assert!(result.is_err());
+        let err_str = result.err().unwrap().to_string();
+        assert_eq!(StatesDef::ARRAY_IDENT_ERR, err_str);
+    }
+
+    impl IdentArray {
+        fn new(state_names: impl Iterator<Item = &'static str>) -> Self {
+            Self(state_names.map(ident).collect())
+        }
+    }
+
+    #[test]
+    fn ident_array_new() {
+        const EXPECTED: [&str; 2] = ["Red", "Green"];
+
+        let actual = IdentArray::new(EXPECTED.into_iter());
+
+        assert_eq!(EXPECTED.len(), actual.0.len());
+        assert_eq!(actual.0[0], EXPECTED[0]);
+        assert_eq!(actual.0[1], EXPECTED[1]);
+    }
+
+    #[test]
+    fn ident_array_not_equal() {
+        let expected = IdentArray::new(["Red", "Green"].into_iter());
+
+        let actual = IdentArray::new(["Blue", "Gold"].into_iter());
+
+        assert_ne!(expected, actual);
+    }
+
+    #[test]
+    fn ident_array_not_equal_different_lens() {
+        let expected = IdentArray::new(["Red", "Green"].into_iter());
+
+        let actual = IdentArray::new(["Red"].into_iter());
+
+        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.into_iter());
+
+        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);
+    }
+
+    impl Transition {
+        fn new(
+            in_state: State,
+            in_msg: Option<Message>,
+            out_states: impl Iterator<Item = State>,
+            out_msgs: impl Iterator<Item = Dest>,
+        ) -> Self {
+            Self {
+                in_state,
+                in_msg,
+                out_states: StatesList(out_states.collect()),
+                out_msgs: DestList(out_msgs.collect()),
+            }
+        }
+    }
+
+    #[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, iter::empty()),
+            None,
+            [EXPECTED_OUT_STATE].map(State::new_empty_owned).into_iter(),
+            iter::empty(),
+        );
+
+        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, iter::empty()),
+            Some(Message::new(EXPECTED_IN_MSG, false, iter::empty())),
+            [EXPECTED_OUT_STATE].map(State::new_empty_owned).into_iter(),
+            iter::empty(),
+        );
+
+        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, iter::empty()),
+            Some(Message::new(EXPECTED_IN_MSG, true, iter::empty())),
+            [EXPECTED_OUT_STATE].map(State::new_empty_owned).into_iter(),
+            iter::empty(),
+        );
+
+        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, iter::empty()),
+            Some(Message::new(
+                EXPECTED_IN_MSG,
+                true,
+                EXPECTED_OWNED_STATES.into_iter(),
+            )),
+            [EXPECTED_OUT_STATE].map(State::new_empty_owned).into_iter(),
+            iter::empty(),
+        );
+
+        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, iter::empty()),
+            None,
+            [EXPECTED_OUT_STATE].map(State::new_empty_owned).into_iter(),
+            [Dest::new(
+                DestinationState::Individual(State::new(EXPECTED_DEST_STATE, iter::empty())),
+                Message::new(EXPECTED_DEST_MSG, false, iter::empty()),
+            )]
+            .into_iter(),
+        );
+
+        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, iter::empty()),
+            Some(Message::new(EXPECTED_IN_MSG, false, iter::empty())),
+            EXPECTED_OUT_STATES.map(State::new_empty_owned).into_iter(),
+            EXPECTED_DESTS
+                .map(|(l, r)| {
+                    Dest::new(
+                        DestinationState::Individual(State::new(l, iter::empty())),
+                        Message::new(r, false, iter::empty()),
+                    )
+                })
+                .into_iter(),
+        );
+
+        let actual = parse_str::<Transition>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    impl State {
+        fn new(state_trait: &str, owned_states: impl Iterator<Item = &'static str>) -> Self {
+            Self {
+                state_trait: ident(state_trait),
+                owned_states: IdentArray::new(owned_states),
+            }
+        }
+
+        fn new_empty_owned(state_trait: &str) -> Self {
+            Self::new(state_trait, iter::empty())
+        }
+    }
+
+    #[test]
+    fn state_parse_no_owned_states() {
+        const EXPECTED_TRAIT: &str = "Contained";
+        let expected = State::new(EXPECTED_TRAIT, iter::empty());
+
+        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.into_iter());
+
+        let actual = parse_str::<State>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    impl Dest {
+        fn new(state: DestinationState, msg: Message) -> Self {
+            Self { state, msg }
+        }
+    }
+
+    #[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, iter::empty())),
+            Message::new(EXPECTED_MSG, false, iter::empty()),
+        );
+
+        let actual = parse_str::<Dest>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+
+    #[test]
+    fn destination_state_parse_regular() {
+        const EXPECTED_DEST_STATE: &str = "Listening";
+        let expected = DestinationState::Individual(State::new(EXPECTED_DEST_STATE, iter::empty()));
+
+        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.into_iter(),
+        ));
+
+        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(Ident::new(EXPECTED_DEST_STATE, Span::call_site()));
+        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);
+    }
+
+    impl Message {
+        fn new(
+            msg_type: &str,
+            is_reply: bool,
+            owned_states: impl Iterator<Item = &'static str>,
+        ) -> Self {
+            Self {
+                msg_type: ident(msg_type),
+                is_reply,
+                owned_states: IdentArray::new(owned_states),
+            }
+        }
+    }
+
+    #[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.into_iter(),
+        );
+
+        assert_eq!(actual.msg_type, EXPECTED_MSG_TYPE);
+        assert_eq!(actual.is_reply, EXPECTED_IS_REPLY);
+        assert_eq!(actual.owned_states.0.len(), EXPECTED_OWNED_STATES.len());
+        assert_eq!(actual.owned_states.0[0], EXPECTED_OWNED_STATES[0]);
+        assert_eq!(actual.owned_states.0[1], EXPECTED_OWNED_STATES[1]);
+    }
+
+    #[test]
+    fn message_parse_regular() {
+        const EXPECTED_MSG: &str = "Write";
+        let expected = Message::new(EXPECTED_MSG, false, iter::empty());
+
+        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, iter::empty());
+
+        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!(Message::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.into_iter());
+
+        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.into_iter());
+
+        let actual = parse_str::<Message>(&input).unwrap();
+
+        assert_eq!(expected, actual);
+    }
+}

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

@@ -0,0 +1,8 @@
+use super::Protocol;
+
+impl Protocol {
+    pub(crate) fn validate(&self) -> syn::Result<()> {
+        // TODO: Validate input.
+        Ok(())
+    }
+}

+ 4 - 4
crates/btrun/src/fs_proto.rs

@@ -1,11 +1,11 @@
 use btproto::protocol;
-use serde::{Serialize, Deserialize};
+use serde::{Deserialize, Serialize};
 
-use crate::{sector_proto::FileId, CallMsg, ActorName};
+use crate::{sector_proto::FileId, ActorName, CallMsg};
 
 #[derive(Serialize, Deserialize)]
 pub struct Open {
-    id: FileId
+    id: FileId,
 }
 
 impl CallMsg for Open {
@@ -39,4 +39,4 @@ protocol! {
 
     FileHandle[Opened] -> End, >Opened!Close;
     Opened?Close -> End;
-}
+}

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

@@ -1,7 +1,7 @@
 #![feature(impl_trait_in_assoc_type)]
 
-pub mod sector_proto;
 pub mod fs_proto;
+pub mod sector_proto;
 
 use std::{
     any::Any,
@@ -818,7 +818,7 @@ mod tests {
         ];
         ClientInit?Activate -> SentPing, >Listening!Ping;
         ServerInit?Activate -> Listening;
-        Listening?Ping -> End, SentPing!Ping::Reply;
+        Listening?Ping -> End, >SentPing!Ping::Reply;
         SentPing?Ping::Reply -> End;
     }
     //
@@ -1031,12 +1031,12 @@ mod tests {
             Choosing,
         ];
         AgencyInit?Activate -> Listening;
-        Choosing -> Choosing, Listening!Query|Accept|Reject;
-        Listening?Query -> Listening, Choosing!Query::Reply;
+        Choosing -> Choosing, >Listening!Query, Listening!Accept, Listening!Reject;
+        Listening?Query -> Listening, >Choosing!Query::Reply;
         Choosing?Query::Reply -> Choosing;
-        Listening?Accept -> End, Choosing!Accept::Reply;
+        Listening?Accept -> End, >Choosing!Accept::Reply;
         Choosing?Accept::Reply -> End;
-        Listening?Reject -> End, Choosing!Reject:Reply;
+        Listening?Reject -> End, >Choosing!Reject::Reply;
         Choosing?Reject::Reply -> End;
     }
 }

+ 6 - 6
crates/btrun/src/sector_proto.rs

@@ -1,9 +1,9 @@
 //! Types which define the protocol used by the sector layer.
 
-use btlib::{BlockMeta, crypto::merkle_stream::VariantMerkleTree, Inode};
+use btlib::{crypto::merkle_stream::VariantMerkleTree, BlockMeta, Inode};
 use btproto::protocol;
 
-use crate::{Serialize, Deserialize, CallMsg};
+use crate::{CallMsg, Deserialize, Serialize};
 
 #[derive(Serialize, Deserialize)]
 pub struct SectorMsg {
@@ -46,7 +46,7 @@ pub enum WriteOperation {
     Data {
         meta: Box<BlockMeta>,
         contents: Vec<u8>,
-    }
+    },
 }
 
 #[derive(Serialize, Deserialize)]
@@ -60,7 +60,7 @@ protocol! {
     let name = SectorProtocol;
     let states = [ServerInit, Listening, Client];
     ServerInit?Activate -> Listening;
-    Client -> Client, service(Listening)!SectorMsg;
-    Listening?SectorMsg -> Listening, Client!SectorMsg::Reply;
+    Client -> Client, >service(Listening)!SectorMsg;
+    Listening?SectorMsg -> Listening, >Client!SectorMsg::Reply;
     Client?SectorMsg::Reply -> Client;
-}
+}