Переглянути джерело

Implemented a parser for the protocol language using syn.

Matthew Carr 1 рік тому
батько
коміт
20bf5b6bd2

+ 17 - 12
Cargo.lock

@@ -70,7 +70,7 @@ checksum = "b9ccdd8f2a161be9bd5c023df56f1b2a0bd1d83872ae53b71a84a12c9bf6e842"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.42",
 ]
 
 [[package]]
@@ -414,6 +414,11 @@ dependencies = [
 [[package]]
 name = "btproto"
 version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.42",
+]
 
 [[package]]
 name = "btproto-tests"
@@ -1048,7 +1053,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.42",
 ]
 
 [[package]]
@@ -1900,7 +1905,7 @@ dependencies = [
  "proc-macro2",
  "proc-macro2-diagnostics",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.42",
 ]
 
 [[package]]
@@ -1986,7 +1991,7 @@ checksum = "39407670928234ebc5e6e580247dd567ad73a3578460c5990f9503df207e8f07"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.42",
 ]
 
 [[package]]
@@ -2099,9 +2104,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",
 ]
@@ -2114,7 +2119,7 @@ checksum = "606c4ba35817e2922a308af55ad51bab3645b59eae5c570d4a6cf07e36bd493b"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.42",
  "version_check",
  "yansi",
 ]
@@ -2188,9 +2193,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",
 ]
@@ -2725,9 +2730,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",
@@ -3131,7 +3136,7 @@ checksum = "3f67b459f42af2e6e1ee213cb9da4dbd022d3320788c3fb3e1b893093f1e45da"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.22",
+ "syn 2.0.42",
 ]
 
 [[package]]

+ 3 - 0
crates/btproto/Cargo.toml

@@ -9,3 +9,6 @@ proc-macro = true
 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
 
 [dependencies]
+proc-macro2 = "1.0.71"
+quote = "1.0.33"
+syn = "2.0.42"

+ 284 - 1
crates/btproto/src/lib.rs

@@ -1,7 +1,290 @@
 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,
+};
 
+/// 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 )* ','? ']' ;
+/// transition : state ( '?' message )?  "->" states_list ( '>' dest_list )? ';' ;
+/// states_list : state ',' ( state ',' )* ;
+/// state : ident states_array? ;
+/// message : ident ( "::" "Reply" )? states_array? ;
+/// dest_list : dest ( ',' dest )* ;
+/// dest : dest_state '!' message
+/// dest_state : ( "service" '(' ident ')' ) | state ;
+/// ```
 #[proc_macro]
-pub fn protocol(_tokens: TokenStream) -> TokenStream {
+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);
+    }
+}

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

@@ -0,0 +1,122 @@
+use btproto::protocol;
+
+struct End;
+
+struct Ping;
+
+#[test]
+fn minimal_syntax() {
+    // This is the token stream that should be passed to the protocol macro:
+    // Ident { ident: "let", span: #0 bytes(91..94) }
+    // Ident { ident: "name", span: #0 bytes(95..99) }
+    // Punct { ch: '=', spacing: Alone, span: #0 bytes(100..101) }
+    // Ident { ident: "Minimal", span: #0 bytes(102..109) }
+    // Punct { ch: ';', spacing: Alone, span: #0 bytes(109..110) }
+    // Ident { ident: "let", span: #0 bytes(119..122) }
+    // Ident { ident: "states", span: #0 bytes(123..129) }
+    // Punct { ch: '=', spacing: Alone, span: #0 bytes(130..131) }
+    // Group { delimiter: Bracket,
+    //    stream: TokenStream [Ident { ident: "Init", span: #0 bytes(133..137) }],
+    //    span: #0 bytes(132..138)
+    // }
+    // Punct { ch: ';', spacing: Alone, span: #0 bytes(138..139) }
+    // Ident { ident: "Init", span: #0 bytes(148..152) }
+    // Punct { ch: '?', spacing: Alone, span: #0 bytes(152..153) }
+    // Ident { ident: "Activate", span: #0 bytes(153..161) }
+    // Punct { ch: '-', spacing: Joint, span: #0 bytes(162..163) }
+    // Punct { ch: '>', spacing: Alone, span: #0 bytes(163..164) }
+    // Ident { ident: "End", span: #0 bytes(165..168) }
+    // Punct { ch: ';', spacing: Alone, span: #0 bytes(168..169) }
+    protocol! {
+        let name = Minimal;
+        let states = [Init];
+        Init?Activate -> End;
+    }
+}
+
+#[test]
+fn reply() {
+    // Ident { ident: "let", span: #0 bytes(1529..1532) }
+    // Ident { ident: "name", span: #0 bytes(1533..1537) }
+    // Punct { ch: '=', spacing: Alone, span: #0 bytes(1538..1539) }
+    // Ident { ident: "Reply", span: #0 bytes(1540..1545) }
+    // Punct { ch: ';', spacing: Alone, span: #0 bytes(1545..1546) }
+    // Ident { ident: "let", span: #0 bytes(1555..1558) }
+    // Ident { ident: "states", span: #0 bytes(1559..1565) }
+    // Punct { ch: '=', spacing: Alone, span: #0 bytes(1566..1567) }
+    // Group {
+    //    delimiter: Bracket,
+    //    stream: TokenStream [
+    //        Ident { ident: "ServerInit", span: #0 bytes(1582..1592) },
+    //        Punct { ch: ',', spacing: Alone, span: #0 bytes(1592..1593) },
+    //        Ident { ident: "Listening", span: #0 bytes(1594..1603) },
+    //        Punct { ch: ',', spacing: Alone, span: #0 bytes(1603..1604) },
+    //        Ident { ident: "Client", span: #0 bytes(1617..1623) },
+    //        Punct { ch: ',', spacing: Alone, span: #0 bytes(1623..1624) },
+    //        Ident { ident: "Waiting", span: #0 bytes(1625..1632) },
+    //        Punct { ch: ',', spacing: Alone, span: #0 bytes(1632..1633) }
+    //    ],
+    //    span: #0 bytes(1568..1643)
+    // }
+    // Punct { ch: ';', spacing: Alone, span: #0 bytes(1643..1644) }
+    // Ident { ident: "ServerInit", span: #0 bytes(1653..1663) }
+    // Punct { ch: '?', spacing: Alone, span: #0 bytes(1663..1664) }
+    // Ident { ident: "Activate", span: #0 bytes(1664..1672) }
+    // Punct { ch: '-', spacing: Joint, span: #0 bytes(1673..1674) }
+    // Punct { ch: '>', spacing: Alone, span: #0 bytes(1674..1675) }
+    // Ident { ident: "Listening", span: #0 bytes(1676..1685) }
+    // Punct { ch: ';', spacing: Alone, span: #0 bytes(1685..1686) }
+    // Ident { ident: "Client", span: #0 bytes(1695..1701) }
+    // Punct { ch: '-', spacing: Joint, span: #0 bytes(1702..1703) }
+    // Punct { ch: '>', spacing: Alone, span: #0 bytes(1703..1704) }
+    // Ident { ident: "Waiting", span: #0 bytes(1705..1712) }
+    // Punct { ch: ',', spacing: Alone, span: #0 bytes(1712..1713) }
+    // Punct { ch: '>', spacing: Alone, span: #0 bytes(1714..1715) }
+    // Ident { ident: "service", span: #0 bytes(1715..1722) }
+    // Group {
+    //     delimiter: Parenthesis,
+    //     stream: TokenStream [
+    //         Ident { ident: "Listening", span: #0 bytes(1723..1732) }
+    //     ],
+    //    span: #0 bytes(1722..1733)
+    // }
+    // Punct { ch: '!', spacing: Alone, span: #0 bytes(1733..1734) }
+    // Ident { ident: "Ping", span: #0 bytes(1734..1738) }
+    // Punct { ch: ';', spacing: Alone, span: #0 bytes(1738..1739) }
+    // Ident { ident: "Listening", span: #0 bytes(1748..1757) }
+    // Punct { ch: '?', spacing: Alone, span: #0 bytes(1757..1758) }
+    // Ident { ident: "Ping", span: #0 bytes(1758..1762) }
+    // Punct { ch: '-', spacing: Joint, span: #0 bytes(1763..1764) }
+    // Punct { ch: '>', spacing: Alone, span: #0 bytes(1764..1765) }
+    // Ident { ident: "Listening", span: #0 bytes(1766..1775) }
+    // Punct { ch: ',', spacing: Alone, span: #0 bytes(1775..1776) }
+    // Punct { ch: '>', spacing: Alone, span: #0 bytes(1777..1778) }
+    // Ident { ident: "Waiting", span: #0 bytes(1778..1785) }
+    // Punct { ch: '!', spacing: Alone, span: #0 bytes(1785..1786) }
+    // Ident { ident: "Ping", span: #0 bytes(1786..1790) }
+    // Punct { ch: ':', spacing: Joint, span: #0 bytes(1790..1791) }
+    // Punct { ch: ':', spacing: Alone, span: #0 bytes(1791..1792) }
+    // Ident { ident: "Reply", span: #0 bytes(1792..1797) }
+    // Punct { ch: ';', spacing: Alone, span: #0 bytes(1797..1798) }
+    // Ident { ident: "Waiting", span: #0 bytes(1807..1814) }
+    // Punct { ch: '?', spacing: Alone, span: #0 bytes(1814..1815) }
+    // Ident { ident: "Ping", span: #0 bytes(1815..1819) }
+    // Punct { ch: ':', spacing: Joint, span: #0 bytes(1819..1820) }
+    // Punct { ch: ':', spacing: Alone, span: #0 bytes(1820..1821) }
+    // Ident { ident: "Reply", span: #0 bytes(1821..1826) }
+    // Punct { ch: '-', spacing: Joint, span: #0 bytes(1827..1828) }
+    // Punct { ch: '>', spacing: Alone, span: #0 bytes(1828..1829) }
+    // Ident { ident: "End", span: #0 bytes(1830..1833) }
+    // Punct { ch: ';', spacing: Alone, span: #0 bytes(1833..1834) }
+    protocol! {
+        let name = Reply;
+        let states = [
+            ServerInit, Listening,
+            Client, Waiting,
+        ];
+        ServerInit?Activate -> Listening;
+        Client -> Waiting, >service(Listening)!Ping;
+        Listening?Ping -> Listening, >Waiting!Ping::Reply;
+        Waiting?Ping::Reply -> End;
+    }
+}

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

@@ -22,21 +22,21 @@ protocol! {
     ];
     ServerInit?Activate -> Listening;
 
-    Client -> Client, service(Listening)!Query;
-    Listening?Query -> Listening, Client!Query::Reply;
+    Client -> Client, >service(Listening)!Query;
+    Listening?Query -> Listening, >Client!Query::Reply;
     Client?Query::Reply -> Client;
 
-    Client -> Client, service(Listening)!Open;
-    Listening?Open -> Listening, Opened, Client!Open::Reply[Opened];
+    Client -> Client, >service(Listening)!Open;
+    Listening?Open -> Listening, Opened, >Client!Open::Reply[Opened];
     Client?Open::Reply[Opened] -> Client, FileHandle[Opened];
 
     FileInit?Activate -> FileInit;
     FileInit?Open -> Opened;
 
-    FileHandle[Opened] -> FileHandle[Opened], Opened!FileOp;
-    Opened?FileOp -> Opened, Client!FileOp::Reply;
+    FileHandle[Opened] -> FileHandle[Opened], >Opened!FileOp;
+    Opened?FileOp -> Opened, >Client!FileOp::Reply;
     FileHandle?FileOp::Reply -> FileClient;
 
-    FileHandle[Opened] -> End, Opened!Close;
+    FileHandle[Opened] -> End, >Opened!Close;
     Opened?Close -> End;
 }

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

@@ -816,7 +816,7 @@ mod tests {
             ClientInit, SentPing,
             ServerInit, Listening,
         ];
-        ClientInit?Activate -> SentPing, Listening!Ping;
+        ClientInit?Activate -> SentPing, >Listening!Ping;
         ServerInit?Activate -> Listening;
         Listening?Ping -> End, SentPing!Ping::Reply;
         SentPing?Ping::Reply -> End;