|
@@ -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);
|
|
|
+ }
|
|
|
+}
|