Skip to main content

finitomata_macro/
lib.rs

1mod codegen;
2mod parser;
3mod validation;
4
5use proc_macro::TokenStream;
6use quote::quote;
7use syn::{DeriveInput, Expr, Lit, Meta, MetaNameValue, parse_macro_input};
8
9/// Procedural macro for defining finite state machines.
10///
11/// # Example
12/// ```rust,ignore
13/// #[finitomata(
14///     fsm = r#"
15///         [*] --> idle
16///         idle --> |start!| running
17///         running --> |pause| paused
18///         running --> |stop| idle
19///         paused --> |resume| running
20///         idle --> |shutdown| [*]
21///     "#,
22///     syntax = "mermaid",
23///     timer = 5000,
24///     auto_terminate = true,
25/// )]
26/// #[derive(Debug, Clone)]
27/// struct MyWorkflow {
28///     counter: u32,
29/// }
30/// ```
31#[proc_macro_attribute]
32pub fn finitomata(attr: TokenStream, item: TokenStream) -> TokenStream {
33    let input = parse_macro_input!(item as DeriveInput);
34    let struct_name = input.ident.clone();
35
36    // Parse attributes
37    let attr_args = match parse_finitomata_attrs(attr) {
38        Ok(args) => args,
39        Err(err) => {
40            return syn::Error::new(proc_macro2::Span::call_site(), err)
41                .to_compile_error()
42                .into();
43        }
44    };
45
46    // Parse the FSM definition
47    let fsm_def = match &attr_args.syntax {
48        Syntax::Mermaid => parser::parse_mermaid(&attr_args.fsm),
49        Syntax::PlantUml => parser::parse_plantuml(&attr_args.fsm),
50    };
51
52    let fsm = match fsm_def {
53        Ok(fsm) => fsm,
54        Err(err) => {
55            return syn::Error::new(
56                proc_macro2::Span::call_site(),
57                format!("FSM parse error: {err}"),
58            )
59            .to_compile_error()
60            .into();
61        }
62    };
63
64    // Validate the FSM
65    if let Err(errors) = validation::validate(&fsm) {
66        let msg = errors.join("; ");
67        return syn::Error::new(
68            proc_macro2::Span::call_site(),
69            format!("FSM validation error: {msg}"),
70        )
71        .to_compile_error()
72        .into();
73    }
74
75    // Generate code
76    let config = codegen::CodegenConfig {
77        struct_name: struct_name.clone(),
78        timer: attr_args.timer,
79        auto_terminate: attr_args.auto_terminate,
80        cache_state: attr_args.cache_state,
81    };
82
83    let generated = codegen::generate(&fsm, &config);
84
85    let expanded = quote! {
86        #input
87
88        #generated
89    };
90
91    expanded.into()
92}
93
94#[derive(Debug, Clone, Copy)]
95enum Syntax {
96    Mermaid,
97    PlantUml,
98}
99
100struct FinitomataAttrs {
101    fsm: String,
102    syntax: Syntax,
103    timer: Option<u64>,
104    auto_terminate: bool,
105    cache_state: bool,
106}
107
108struct AttrList(syn::punctuated::Punctuated<Meta, syn::Token![,]>);
109
110impl syn::parse::Parse for AttrList {
111    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
112        Ok(Self(syn::punctuated::Punctuated::parse_terminated(input)?))
113    }
114}
115
116fn parse_finitomata_attrs(attr: TokenStream) -> Result<FinitomataAttrs, String> {
117    let attr2: proc_macro2::TokenStream = attr.into();
118
119    let mut fsm = None;
120    let mut syntax = Syntax::Mermaid;
121    let mut timer = None;
122    let mut auto_terminate = false;
123    let mut cache_state = true;
124
125    let parsed: AttrList =
126        syn::parse2(attr2).map_err(|e| format!("failed to parse attributes: {e}"))?;
127
128    for meta in parsed.0 {
129        if let Meta::NameValue(MetaNameValue { path, value, .. }) = meta {
130            let key = path.get_ident().map(|i| i.to_string()).unwrap_or_default();
131
132            match key.as_str() {
133                "fsm" => {
134                    if let Expr::Lit(expr_lit) = &value
135                        && let Lit::Str(lit) = &expr_lit.lit
136                    {
137                        fsm = Some(lit.value());
138                    }
139                }
140                "syntax" => {
141                    if let Expr::Lit(expr_lit) = &value
142                        && let Lit::Str(lit) = &expr_lit.lit
143                    {
144                        syntax = match lit.value().as_str() {
145                            "plantuml" | "plant_uml" => Syntax::PlantUml,
146                            _ => Syntax::Mermaid,
147                        };
148                    }
149                }
150                "timer" => {
151                    if let Expr::Lit(expr_lit) = &value
152                        && let Lit::Int(lit) = &expr_lit.lit
153                    {
154                        timer = lit.base10_parse::<u64>().ok();
155                    }
156                }
157                "auto_terminate" => {
158                    if let Expr::Lit(expr_lit) = &value
159                        && let Lit::Bool(lit) = &expr_lit.lit
160                    {
161                        auto_terminate = lit.value;
162                    }
163                }
164                "cache_state" => {
165                    if let Expr::Lit(expr_lit) = &value
166                        && let Lit::Bool(lit) = &expr_lit.lit
167                    {
168                        cache_state = lit.value;
169                    }
170                }
171                _ => {}
172            }
173        }
174    }
175
176    let fsm = fsm.ok_or_else(|| "missing required `fsm` attribute".to_string())?;
177
178    Ok(FinitomataAttrs {
179        fsm,
180        syntax,
181        timer,
182        auto_terminate,
183        cache_state,
184    })
185}