obzenflow_fsm_macros/lib.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2// SPDX-FileCopyrightText: 2025-2026 ObzenFlow Contributors
3// https://obzenflow.dev
4
5//! Proc-macro helpers for `obzenflow-fsm`.
6//!
7//! This crate is an implementation detail of `obzenflow-fsm`. End users should depend on
8//! `obzenflow-fsm` and use the re-exported macros from that crate:
9//! - `#[derive(obzenflow_fsm::StateVariant, obzenflow_fsm::EventVariant)]`
10//! - `obzenflow_fsm::fsm! { ... }`
11
12use proc_macro::TokenStream;
13use quote::quote;
14use syn::{parse_macro_input, DeriveInput};
15
16/// Derive `::obzenflow_fsm::StateVariant` for an enum.
17///
18/// The generated `variant_name()` matches on the enum and returns the variant identifier as a
19/// `&'static str`, ignoring any payload.
20#[proc_macro_derive(StateVariant)]
21pub fn derive_state_variant(input: TokenStream) -> TokenStream {
22 let input = parse_macro_input!(input as DeriveInput);
23 let enum_ident = input.ident;
24
25 let data_enum = match input.data {
26 syn::Data::Enum(e) => e,
27 _ => {
28 return syn::Error::new_spanned(
29 enum_ident,
30 "StateVariant can only be derived for enums",
31 )
32 .to_compile_error()
33 .into();
34 }
35 };
36
37 let arms = data_enum.variants.iter().map(|variant| {
38 let ident = &variant.ident;
39 match &variant.fields {
40 syn::Fields::Unit => quote! { Self::#ident => stringify!(#ident), },
41 syn::Fields::Unnamed(_) => quote! { Self::#ident(..) => stringify!(#ident), },
42 syn::Fields::Named(_) => quote! { Self::#ident { .. } => stringify!(#ident), },
43 }
44 });
45
46 let expanded = quote! {
47 impl ::obzenflow_fsm::StateVariant for #enum_ident {
48 fn variant_name(&self) -> &str {
49 match self {
50 #( #arms )*
51 }
52 }
53 }
54 };
55
56 TokenStream::from(expanded)
57}
58
59/// Derive `::obzenflow_fsm::EventVariant` for an enum.
60///
61/// The generated `variant_name()` matches on the enum and returns the variant identifier as a
62/// `&'static str`, ignoring any payload.
63#[proc_macro_derive(EventVariant)]
64pub fn derive_event_variant(input: TokenStream) -> TokenStream {
65 let input = parse_macro_input!(input as DeriveInput);
66 let enum_ident = input.ident;
67
68 let data_enum = match input.data {
69 syn::Data::Enum(e) => e,
70 _ => {
71 return syn::Error::new_spanned(
72 enum_ident,
73 "EventVariant can only be derived for enums",
74 )
75 .to_compile_error()
76 .into();
77 }
78 };
79
80 let arms = data_enum.variants.iter().map(|variant| {
81 let ident = &variant.ident;
82 match &variant.fields {
83 syn::Fields::Unit => quote! { Self::#ident => stringify!(#ident), },
84 syn::Fields::Unnamed(_) => quote! { Self::#ident(..) => stringify!(#ident), },
85 syn::Fields::Named(_) => quote! { Self::#ident { .. } => stringify!(#ident), },
86 }
87 });
88
89 let expanded = quote! {
90 impl ::obzenflow_fsm::EventVariant for #enum_ident {
91 fn variant_name(&self) -> &str {
92 match self {
93 #( #arms )*
94 }
95 }
96 }
97 };
98
99 TokenStream::from(expanded)
100}
101
102/// High-level typed FSM builder DSL.
103///
104/// This macro is re-exported as `obzenflow_fsm::fsm!` and is the recommended way to construct a
105/// `obzenflow_fsm::StateMachine`.
106///
107/// Supported syntax (first pass):
108/// - Top-level `state:`, `event:`, `context:`, `action:`, `initial:`.
109/// - Optional `unhandled => handler;` at top-level.
110/// - `state <State::Variant> { ... }` blocks containing:
111/// - `on <Event::Variant> => handler;`
112/// - `timeout <expr> => handler;`
113/// - `on_entry handler;`
114/// - `on_exit handler;`
115///
116/// Handler shapes:
117/// - `on` handlers take `(&State, &Event, &mut Context)` and return a `Transition`.
118/// - `timeout` handlers take `(&State, &mut Context)` and return a `Transition`.
119/// - `on_entry` / `on_exit` handlers take `(&State, &mut Context)` and return `Vec<Action>`.
120/// - `unhandled` handlers take `(&State, &Event, &mut Context)` and return `()`.
121///
122/// The macro currently expands to a legacy `FsmBuilder` pipeline ending in `.build()`.
123/// As a result, dispatch is performed by `StateVariant::variant_name()` and
124/// `EventVariant::variant_name()` (not by full type paths).
125///
126/// ```rust,ignore
127/// use obzenflow_fsm::{fsm, Transition};
128///
129/// #[derive(Clone, Debug, PartialEq, obzenflow_fsm::StateVariant)]
130/// enum State {
131/// Idle,
132/// }
133///
134/// #[derive(Clone, Debug, obzenflow_fsm::EventVariant)]
135/// enum Event {
136/// Start,
137/// }
138///
139/// #[derive(Clone, Debug, PartialEq)]
140/// enum Action {
141/// Noop,
142/// }
143///
144/// #[derive(Default)]
145/// struct Context;
146///
147/// impl obzenflow_fsm::FsmContext for Context {}
148///
149/// #[async_trait::async_trait]
150/// impl obzenflow_fsm::FsmAction for Action {
151/// type Context = Context;
152///
153/// async fn execute(&self, _ctx: &mut Self::Context) -> obzenflow_fsm::types::FsmResult<()> {
154/// Ok(())
155/// }
156/// }
157///
158/// let _machine = fsm! {
159/// state: State;
160/// event: Event;
161/// context: Context;
162/// action: Action;
163/// initial: State::Idle;
164///
165/// unhandled => |_s: &State, _e: &Event, _ctx: &mut Context| {
166/// Box::pin(async move { Ok(()) })
167/// };
168///
169/// state State::Idle {
170/// on Event::Start => |_s: &State, _e: &Event, _ctx: &mut Context| {
171/// Box::pin(async move {
172/// Ok(Transition {
173/// next_state: State::Idle,
174/// actions: vec![Action::Noop],
175/// })
176/// })
177/// };
178/// }
179/// };
180/// ```
181#[proc_macro]
182pub fn fsm(input: TokenStream) -> TokenStream {
183 // Parse the incoming token stream into our simplified FSM spec,
184 // then expand it into a `FsmBuilder` chain that ends in `.build()`.
185 let input_ts = proc_macro2::TokenStream::from(input);
186
187 let parsed = match crate::parse::FsmSpec::parse(input_ts) {
188 Ok(spec) => spec,
189 Err(err) => return err.to_compile_error().into(),
190 };
191
192 let expanded = crate::codegen::expand_fsm_spec(&parsed);
193 TokenStream::from(expanded)
194}
195
196mod codegen;
197mod parse;