Skip to content

Commit cf1ff54

Browse files
committed
Add GenFsm - Generic Finite State Machine pattern
Implements a process-based FSM inspired by Erlang's gen_statem but using Rust's enum-based states for type safety. Features: - Enum-based states with pattern matching on (state, event) - Mutable data carried across state transitions - State entry/exit callbacks for side effects - Synchronous call support like GenServer - State timeouts (TODO: implement scheduling) Example usage: #[async_trait] impl GenFsm for TrafficLight { type State = LightState; // Red | Yellow | Green type Event = LightEvent; // Timer | Emergency type Data = LightData; // ... async fn handle_event( state: &LightState, event: LightEvent, data: &mut LightData, ) -> EventResult<LightState, ()> { match (state, event) { (Red, Timer) => EventResult::next_state(Green), (Green, Timer) => EventResult::next_state(Yellow), // ... } } }
1 parent 6dd9ffb commit cf1ff54

6 files changed

Lines changed: 914 additions & 0 deletions

File tree

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//! GenFsm error types.
2+
3+
use crate::core::{ExitReason, Pid};
4+
use thiserror::Error;
5+
6+
/// Error returned when starting a GenFsm fails.
7+
#[derive(Debug, Error)]
8+
pub enum StartError {
9+
/// The init callback returned `InitResult::Stop`.
10+
#[error("init stopped: {0}")]
11+
Stop(ExitReason),
12+
/// The init callback returned `InitResult::Ignore`.
13+
#[error("init ignored")]
14+
Ignore,
15+
/// Failed to spawn the process.
16+
#[error("spawn failed")]
17+
SpawnFailed,
18+
}
19+
20+
/// Error returned when a call fails.
21+
#[derive(Debug, Error)]
22+
pub enum CallError {
23+
/// The FSM process is not running.
24+
#[error("process not found: {0:?}")]
25+
ProcessNotFound(Pid),
26+
/// The call timed out.
27+
#[error("timeout")]
28+
Timeout,
29+
/// The FSM stopped while processing the call.
30+
#[error("fsm stopped: {0}")]
31+
Stopped(ExitReason),
32+
/// Failed to decode the reply.
33+
#[error("decode error: {0}")]
34+
DecodeError(String),
35+
/// The FSM was not in expected state.
36+
#[error("invalid state")]
37+
InvalidState,
38+
}
39+
40+
/// Error returned when sending an event fails.
41+
#[derive(Debug, Error)]
42+
pub enum SendEventError {
43+
/// The FSM process is not running.
44+
#[error("process not found: {0:?}")]
45+
ProcessNotFound(Pid),
46+
/// Failed to encode the event.
47+
#[error("encode error")]
48+
EncodeError,
49+
}

crates/starlang/src/gen_fsm/mod.rs

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
//! # GenFsm - Generic Finite State Machine
2+
//!
3+
//! A process-based finite state machine pattern for Starlang, inspired by
4+
//! Erlang's `gen_statem` but with Rust's type safety.
5+
//!
6+
//! # Overview
7+
//!
8+
//! GenFsm provides:
9+
//! - **Enum-based states**: States are variants of an enum, enabling pattern matching
10+
//! - **Event-driven transitions**: State changes happen in response to events
11+
//! - **State-specific handlers**: Different behavior per state
12+
//! - **Mutable data**: Carry data across state transitions
13+
//! - **Process integration**: Runs as a supervised Starlang process
14+
//!
15+
//! # Example: Traffic Light
16+
//!
17+
//! ```ignore
18+
//! use starlang::gen_fsm::{GenFsm, InitResult, EventResult, async_trait};
19+
//! use serde::{Serialize, Deserialize};
20+
//!
21+
//! struct TrafficLight;
22+
//!
23+
//! #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24+
//! enum LightState {
25+
//! Red,
26+
//! Yellow,
27+
//! Green,
28+
//! }
29+
//!
30+
//! #[derive(Debug, Serialize, Deserialize)]
31+
//! enum LightEvent {
32+
//! Timer,
33+
//! EmergencyStop,
34+
//! }
35+
//!
36+
//! #[derive(Debug, Default)]
37+
//! struct LightData {
38+
//! cycle_count: u32,
39+
//! }
40+
//!
41+
//! #[async_trait]
42+
//! impl GenFsm for TrafficLight {
43+
//! type State = LightState;
44+
//! type Event = LightEvent;
45+
//! type Data = LightData;
46+
//! type InitArg = ();
47+
//! type Reply = ();
48+
//!
49+
//! async fn init(_arg: ()) -> InitResult<LightState, LightData> {
50+
//! InitResult::ok(LightState::Red, LightData::default())
51+
//! }
52+
//!
53+
//! async fn handle_event(
54+
//! state: &LightState,
55+
//! event: LightEvent,
56+
//! data: &mut LightData,
57+
//! ) -> EventResult<LightState, ()> {
58+
//! match (state, event) {
59+
//! (LightState::Red, LightEvent::Timer) => {
60+
//! EventResult::next_state(LightState::Green)
61+
//! }
62+
//! (LightState::Green, LightEvent::Timer) => {
63+
//! EventResult::next_state(LightState::Yellow)
64+
//! }
65+
//! (LightState::Yellow, LightEvent::Timer) => {
66+
//! data.cycle_count += 1;
67+
//! EventResult::next_state(LightState::Red)
68+
//! }
69+
//! (_, LightEvent::EmergencyStop) => {
70+
//! EventResult::next_state(LightState::Red)
71+
//! }
72+
//! }
73+
//! }
74+
//! }
75+
//! ```
76+
//!
77+
//! # State Entry/Exit Actions
78+
//!
79+
//! Override `enter_state` and `exit_state` for side effects on transitions:
80+
//!
81+
//! ```ignore
82+
//! async fn enter_state(state: &LightState, data: &mut LightData) {
83+
//! match state {
84+
//! LightState::Red => println!("STOP!"),
85+
//! LightState::Yellow => println!("CAUTION!"),
86+
//! LightState::Green => println!("GO!"),
87+
//! }
88+
//! }
89+
//! ```
90+
91+
#![deny(warnings)]
92+
#![deny(missing_docs)]
93+
94+
mod error;
95+
mod protocol;
96+
mod server;
97+
mod types;
98+
99+
pub use async_trait::async_trait;
100+
pub use error::{CallError, SendEventError, StartError};
101+
pub use server::{GenFsm, call, send_event, start, start_link};
102+
pub use types::{CallResult, EventResult, From, InitResult, StateAction};
103+
104+
// Re-export commonly used types
105+
pub use crate::core::{ExitReason, Pid, Term};
106+
107+
/// Prelude module for convenient imports.
108+
pub mod prelude {
109+
pub use super::{
110+
CallError, CallResult, EventResult, ExitReason, From, GenFsm, InitResult, Pid,
111+
SendEventError, StartError, StateAction, async_trait, call, send_event, start, start_link,
112+
};
113+
}
114+
115+
#[cfg(test)]
116+
mod tests {
117+
use super::*;
118+
use serde::{Deserialize, Serialize};
119+
use std::sync::Arc;
120+
use std::sync::atomic::{AtomicU32, Ordering};
121+
use std::time::Duration;
122+
use tokio::time::sleep;
123+
124+
// Traffic Light FSM for testing
125+
struct TrafficLight;
126+
127+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
128+
enum LightState {
129+
Red,
130+
Yellow,
131+
Green,
132+
}
133+
134+
#[derive(Debug, Clone, Serialize, Deserialize)]
135+
enum LightEvent {
136+
Timer,
137+
Emergency,
138+
}
139+
140+
#[derive(Debug, Clone, Serialize, Deserialize)]
141+
enum LightCall {
142+
GetState,
143+
GetCycleCount,
144+
}
145+
146+
#[derive(Debug, Default)]
147+
struct LightData {
148+
cycle_count: u32,
149+
}
150+
151+
#[async_trait]
152+
impl GenFsm for TrafficLight {
153+
type State = LightState;
154+
type Event = LightEvent;
155+
type Data = LightData;
156+
type InitArg = ();
157+
type Call = LightCall;
158+
type Reply = LightReply;
159+
160+
async fn init(_arg: ()) -> InitResult<LightState, LightData> {
161+
InitResult::ok(LightState::Red, LightData::default())
162+
}
163+
164+
async fn handle_event(
165+
state: &LightState,
166+
event: LightEvent,
167+
data: &mut LightData,
168+
) -> EventResult<LightState, LightReply> {
169+
match (state, event) {
170+
(LightState::Red, LightEvent::Timer) => EventResult::next_state(LightState::Green),
171+
(LightState::Green, LightEvent::Timer) => {
172+
EventResult::next_state(LightState::Yellow)
173+
}
174+
(LightState::Yellow, LightEvent::Timer) => {
175+
data.cycle_count += 1;
176+
EventResult::next_state(LightState::Red)
177+
}
178+
(_, LightEvent::Emergency) => EventResult::next_state(LightState::Red),
179+
}
180+
}
181+
182+
async fn handle_call(
183+
state: &LightState,
184+
request: LightCall,
185+
_from: From,
186+
data: &mut LightData,
187+
) -> CallResult<LightState, LightReply> {
188+
match request {
189+
LightCall::GetState => CallResult::reply(LightReply::State(*state), *state),
190+
LightCall::GetCycleCount => {
191+
CallResult::reply(LightReply::Count(data.cycle_count), *state)
192+
}
193+
}
194+
}
195+
}
196+
197+
#[derive(Debug, Clone, Serialize, Deserialize)]
198+
enum LightReply {
199+
State(LightState),
200+
Count(u32),
201+
}
202+
203+
#[tokio::test]
204+
async fn test_fsm_start() {
205+
crate::process::global::init();
206+
let handle = crate::process::global::handle();
207+
208+
let pid = start::<TrafficLight>(()).await.unwrap();
209+
assert!(handle.alive(pid));
210+
211+
sleep(Duration::from_millis(50)).await;
212+
}
213+
214+
#[tokio::test]
215+
async fn test_fsm_state_transitions() {
216+
crate::process::global::init();
217+
let handle = crate::process::global::handle();
218+
219+
let pid = start::<TrafficLight>(()).await.unwrap();
220+
221+
// Helper to get current state via call
222+
let get_state = Arc::new(AtomicU32::new(0)); // 0=Red, 1=Yellow, 2=Green
223+
224+
// Initial state should be Red
225+
{
226+
let get_state = get_state.clone();
227+
handle.spawn(move || async move {
228+
if let Ok(LightReply::State(state)) =
229+
call::<TrafficLight>(pid, LightCall::GetState, Duration::from_secs(5)).await
230+
{
231+
let val = match state {
232+
LightState::Red => 0,
233+
LightState::Yellow => 1,
234+
LightState::Green => 2,
235+
};
236+
get_state.store(val, Ordering::SeqCst);
237+
}
238+
});
239+
}
240+
sleep(Duration::from_millis(50)).await;
241+
assert_eq!(get_state.load(Ordering::SeqCst), 0); // Red
242+
243+
// Send Timer event: Red -> Green
244+
send_event::<TrafficLight>(pid, LightEvent::Timer).unwrap();
245+
sleep(Duration::from_millis(50)).await;
246+
247+
{
248+
let get_state = get_state.clone();
249+
handle.spawn(move || async move {
250+
if let Ok(LightReply::State(state)) =
251+
call::<TrafficLight>(pid, LightCall::GetState, Duration::from_secs(5)).await
252+
{
253+
let val = match state {
254+
LightState::Red => 0,
255+
LightState::Yellow => 1,
256+
LightState::Green => 2,
257+
};
258+
get_state.store(val, Ordering::SeqCst);
259+
}
260+
});
261+
}
262+
sleep(Duration::from_millis(50)).await;
263+
assert_eq!(get_state.load(Ordering::SeqCst), 2); // Green
264+
}
265+
266+
#[tokio::test]
267+
async fn test_fsm_data_persistence() {
268+
crate::process::global::init();
269+
let handle = crate::process::global::handle();
270+
271+
let pid = start::<TrafficLight>(()).await.unwrap();
272+
273+
// Complete one full cycle: Red -> Green -> Yellow -> Red
274+
send_event::<TrafficLight>(pid, LightEvent::Timer).unwrap(); // -> Green
275+
sleep(Duration::from_millis(20)).await;
276+
send_event::<TrafficLight>(pid, LightEvent::Timer).unwrap(); // -> Yellow
277+
sleep(Duration::from_millis(20)).await;
278+
send_event::<TrafficLight>(pid, LightEvent::Timer).unwrap(); // -> Red (cycle++)
279+
sleep(Duration::from_millis(50)).await;
280+
281+
// Check cycle count
282+
let cycle_count = Arc::new(AtomicU32::new(0));
283+
{
284+
let cycle_count = cycle_count.clone();
285+
handle.spawn(move || async move {
286+
if let Ok(LightReply::Count(count)) =
287+
call::<TrafficLight>(pid, LightCall::GetCycleCount, Duration::from_secs(5))
288+
.await
289+
{
290+
cycle_count.store(count, Ordering::SeqCst);
291+
}
292+
});
293+
}
294+
sleep(Duration::from_millis(50)).await;
295+
assert_eq!(cycle_count.load(Ordering::SeqCst), 1);
296+
}
297+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
//! GenFsm protocol messages.
2+
3+
use crate::core::{Pid, Ref};
4+
use serde::{Deserialize, Serialize};
5+
6+
/// Messages sent to a GenFsm process.
7+
#[derive(Debug, Clone, Serialize, Deserialize)]
8+
pub enum FsmMessage {
9+
/// An event to be processed.
10+
Event(Vec<u8>),
11+
/// A synchronous call request.
12+
Call {
13+
/// The calling process.
14+
from: Pid,
15+
/// Unique reference for this call.
16+
reference: Ref,
17+
/// The encoded call request.
18+
request: Vec<u8>,
19+
},
20+
/// A reply to a pending call.
21+
Reply {
22+
/// The reference of the original call.
23+
reference: Ref,
24+
/// The encoded reply.
25+
reply: Vec<u8>,
26+
},
27+
/// State timeout fired.
28+
StateTimeout,
29+
/// Generic timeout fired.
30+
GenericTimeout(String),
31+
}

0 commit comments

Comments
 (0)