//! DSL implementation for defining finite state machines for `rust-fsm`. See //! more in the `rust-fsm` crate documentation. #![recursion_limit = "128"] extern crate proc_macro; use proc_macro::TokenStream; use quote::{quote, ToTokens}; use syn::*; mod parser; mod variant; use variant::Variant; use crate::{parser::StateMachineDef, variant::Final}; /// The full information about a state transition. Used to unify the /// represantion of the simple and the compact forms. struct Transition<'a> { initial_state: &'a Variant, input_value: &'a Variant, final_state: &'a Final, output: &'a Option, } fn attrs_to_token_stream(attrs: Vec) -> proc_macro2::TokenStream { let attrs = attrs.into_iter().map(ToTokens::into_token_stream); attrs.collect() } #[proc_macro] /// Produce a state machine definition from the provided `rust-fmt` DSL /// description. pub fn state_machine(tokens: TokenStream) -> TokenStream { let StateMachineDef { doc, state_name: (state_attrs, state_visibility, state_name), input_name: (input_attrs, input_visibility, input_name), output_name: (output_attrs, output_visibility, output_name), transitions, } = parse_macro_input!(tokens as parser::StateMachineDef); let doc = attrs_to_token_stream(doc); if transitions.is_empty() { let output = quote! { compile_error!("rust-fsm: at least one state transition must be provided"); }; return output.into(); } let transitions = transitions.iter().flat_map(|def| { def.transitions.iter().map(move |transition| Transition { initial_state: &def.initial_state, input_value: &transition.input_value, final_state: &transition.final_state, output: &transition.output, }) }); // fn id(x: impl std::hash::Hash) -> u64 { // use std::hash::BuildHasher; // rustc_hash::FxSeededState::with_seed(5).hash_one(x) // } let mut states = vec![]; let mut inputs = vec![]; let mut outputs = vec![]; let mut transition_cases = vec![]; #[cfg(feature = "diagram")] let mut mermaid_diagram = format!( "///```mermaid ///stateDiagram-v2\n", ); for transition in transitions { let Transition { initial_state, final_state, input_value, output, } = transition; // #[cfg(feature = "diagram")] // writeln!( // mermaid_diagram, // "/// {}: {initial_state}", // id(&initial_state) // ) // .unwrap(); // #[cfg(feature = "diagram")] // writeln!( // mermaid_diagram, // "/// {}: {final_state}", // id(&final_state) // ) // .unwrap(); use std::fmt::Write; #[cfg(feature = "diagram")] write!( mermaid_diagram, "/// {}", &format!( "{:?}", format!( "{initial_state} --> {final_state}: {}", input_value.match_on() ) ) .trim_matches('"'), ) .unwrap(); let (initial_, guard_) = initial_state.separate(); let final_ = final_state .reduce() .map_or(initial_state.match_on(), |x| quote! { #x }); let (input_, guard) = input_value.separate(); let guard = guard_ .clone() .zip(guard.clone()) .map(|(x, y)| { quote! { if #x && #y } }) .or(guard_.or(guard).map(|x| quote! { if #x })) .unwrap_or_default(); // let input_ = input_value.match_on(); // let final_state_ = final_state.match_on(); let output_ = output .as_ref() .map(|x| { #[cfg(feature = "diagram")] mermaid_diagram.push_str(&format!(" [\"{x}\"]")); let output = x.reduce().unwrap(); quote! { ::core::option::Option::Some(Self::Output::#output) } }) .unwrap_or(quote! { ::core::option::Option::None }); // let x = format!("{}, {} {} => {}", initial_, input_, guard, output_); transition_cases.push(quote! { (Self::#initial_, Self::Input::#input_) #guard => { ::core::result::Result::Ok((Self::#final_, #output_)) } }); #[cfg(feature = "diagram")] mermaid_diagram.push('\n'); states.push(initial_state.clone()); states.extend(final_state.clone().variant()); inputs.push(input_value.clone()); if let Some(output) = output { outputs.push(output.clone().variant().unwrap()); } } #[cfg(feature = "diagram")] mermaid_diagram.push_str("///```"); #[cfg(feature = "diagram")] let mermaid_diagram: proc_macro2::TokenStream = mermaid_diagram .replace("::", "#58;#58;") .replace('(', "#40;") .replace(')', "#41;") .replace('[', "#91;") .replace(']', "#93;") .replace('|', "#124;") .replace("Default", "def") .parse() .unwrap(); let input_generics = input_name.g(); let input_impl = variant::tokenize(&inputs, |x| { let attrs = attrs_to_token_stream(input_attrs); input_name.tokenize(|f| { quote! { #attrs #input_visibility enum #f #input_generics { #(#x),* } } }) }); let input_name = input_name.path(); let state_impl = variant::tokenize(&states, |x| { let attrs = attrs_to_token_stream(state_attrs.clone()); state_name.tokenize(|f| { quote! { #attrs #state_visibility enum #f { #(#x),* } } }) }); let state_name = state_name.path(); let output_generics = output_name.g(); let output_impl = variant::tokenize(&outputs, |outputs| { let attrs = attrs_to_token_stream(output_attrs); output_name.tokenize(|output_name| { quote! { #attrs #output_visibility enum #output_name #output_generics { #(#outputs),* } } }) }); let output_name = output_name.path(); #[cfg(feature = "diagram")] let diagram = quote! { #[cfg_attr(doc, ::rust_fsm::aquamarine)] #mermaid_diagram }; #[cfg(not(feature = "diagram"))] let diagram = quote!(); let output = quote! { #input_impl #doc #diagram #state_impl #output_impl impl ::rust_fsm::StateMachine for #state_name { type Input<'i> = #input_name #input_generics; type Output<'i> = #output_name #output_generics; fn transition(self, input: Self::Input<'_>) -> ::core::result::Result< (Self, ::core::option::Option>), ::rust_fsm::TransitionImpossibleError> > { match (self, input) { #(#transition_cases)* (state, input) => ::core::result::Result::Err(::rust_fsm::TransitionImpossibleError { state, input, }), } } } }; output.into() }