├── .gitignore ├── .travis.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── agent │ └── mod.rs ├── ast.rs ├── interpreter.rs └── lib.rs └── tests ├── assign.rs ├── choice.rs ├── pingpong.rs ├── stateid.rs └── test.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | **/*.rs.bk 3 | Cargo.lock 4 | .*.swp 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "statechart" 3 | description = "statecharts: hierarchical, reactive state machines" 4 | license = "MIT" 5 | version = "0.0.8" 6 | documentation = "https://docs.rs/statechart" 7 | repository = "https://github.com/cmars/statechart" 8 | authors = ["Casey Marshall "] 9 | keywords = ["statechart", "state-machine", "reactive", "event-driven"] 10 | readme = "README.md" 11 | 12 | [badges] 13 | travis-ci = { repository = "cmars/statechart" } 14 | 15 | [dependencies] 16 | chrono = "0.4.0" 17 | derive_builder = "0.5.0" 18 | env_logger = "0.4.3" 19 | futures = "0.1" 20 | log = "0.3.8" 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2017 Casey Marshall 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # statechart 2 | [![Build Status](https://travis-ci.org/cmars/statechart.svg?branch=master)](https://travis-ci.org/cmars/statechart) 3 | [![Crate](https://img.shields.io/crates/v/statechart.svg)](https://crates.io/crates/statechart) 4 | 5 | A rust implementation of statecharts. Statecharts are a visual approach to expressing hierarchical state machines, useful for describing reactive, event-driven systems. Statecharts were originally introduced in [Harel87](http://www.wisdom.weizmann.ac.il/~dharel/SCANNED.PAPERS/Statecharts.pdf). [Harel07](http://www.wisdom.weizmann.ac.il/~harel/papers/Statecharts.History.pdf) gives background and context to the innovation and development of statecharts. For technical context, application to MDSD and practical systems design, see [_D. Harel and M. Politi, Modeling Reactive Systems with Statecharts: The STATEMATE Approach, (with M. Politi)_](http://www.wisdom.weizmann.ac.il/~harel/reactive_systems.html). 6 | 7 | ## Current project focus 8 | 9 | This crate initially aims to provide a statechart document model and interpreter, influenced by, and probably mostly isomorphic to the concepts, entities and behaviors described in the [W3C State Chart XML specification](https://www.w3.org/TR/scxml/). However, the focus is not full scxml compliance, but the following use-cases: 10 | 11 | - Rust macros for expressing and executing statecharts from Rust code directly. 12 | - Interpreter compatibility with futures & streams to support asynchronous execution. 13 | - Automating some useful, operational tasks with software agents, expressed as statecharts. 14 | - Personal assistants 15 | - Automated software operations, auto-{healing,scaling,alerting,reconfiguring} 16 | - Privacy and security, "situational awareness" for networked applications & devices 17 | - Performance, after usefulness and correctness 18 | 19 | ## Possible areas of further interest 20 | 21 | Applications which I am not actively developing for yet, but could be interesting 22 | and useful contributions: 23 | 24 | - `no_std` support for embedded applications. The kind of thing statecharts were invented for! 25 | - A compiler backend. Target LLVM or an FPGA! 26 | - SCXML compatibility, where it doesn't conflict with the above goals, and there's a good use for the angle brackets. 27 | 28 | --- 29 | 30 | Copyright 2017 Casey Marshall 31 | 32 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 33 | 34 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 35 | 36 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 37 | -------------------------------------------------------------------------------- /src/agent/mod.rs: -------------------------------------------------------------------------------- 1 | use futures::{Async, Poll}; 2 | use futures::stream::Stream; 3 | use futures::sync::mpsc; 4 | 5 | use ast::Context; 6 | use interpreter::{Event, Fault, Interpreter, Status}; 7 | 8 | pub struct Agent<'a> { 9 | pub sender: mpsc::Sender, 10 | receiver: mpsc::Receiver, 11 | ctx: &'a Context, 12 | it: Interpreter, 13 | eos: bool, 14 | } 15 | 16 | impl<'a> Stream for Agent<'a> { 17 | type Item = Status; 18 | type Error = Fault; 19 | fn poll(&mut self) -> Poll, Self::Error> { 20 | loop { 21 | let status = self.it.step(self.ctx)?; 22 | match &status { 23 | &Status::Done(_) => { 24 | if !self.eos { 25 | self.eos = true; 26 | return Ok(Async::Ready(Some(status))); 27 | } else { 28 | return Ok(Async::Ready(None)); 29 | } 30 | } 31 | &Status::Blocked => { 32 | match self.receiver.poll() { 33 | Ok(Async::Ready(Some(event))) => { 34 | self.it.push_event(event); 35 | continue; 36 | } 37 | Ok(Async::Ready(None)) => return Err(Fault::BlockedIndefinitely), 38 | Ok(Async::NotReady) => return Ok(Async::NotReady), 39 | Err(_) => return Err(Fault::BlockedIndefinitely), 40 | } 41 | } 42 | _ => return Ok(Async::Ready(Some(status))), 43 | } 44 | } 45 | } 46 | } 47 | 48 | impl<'a> Agent<'a> { 49 | pub fn new(ctx: &'a Context, it: Interpreter) -> Agent { 50 | let (sender, receiver) = mpsc::channel(0); 51 | Agent { 52 | sender: sender, 53 | receiver: receiver, 54 | ctx: ctx, 55 | it: it, 56 | eos: false, 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ast.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, HashSet}; 2 | use std::iter; 3 | use std::slice::{Iter, IterMut}; 4 | 5 | use interpreter::{Action, Condition, Output}; 6 | use interpreter::{Empty, True}; 7 | 8 | pub type StateID = Vec; 9 | 10 | pub type StateLabel = String; 11 | 12 | pub trait Node { 13 | fn id(&self) -> &StateID; 14 | fn label(&self) -> &StateLabel; 15 | fn substate(&self, label: &str) -> Option<&State>; 16 | fn initial(&self) -> Option<&StateLabel>; 17 | fn parent(&self) -> Option<&StateID>; 18 | fn on_entry(&self) -> &Vec; 19 | fn on_exit(&self) -> &Vec; 20 | } 21 | 22 | pub trait ActiveNode: Node { 23 | fn transitions(&self) -> &Vec; 24 | } 25 | 26 | #[derive(Debug, Clone, Builder)] 27 | pub struct Atomic { 28 | #[builder(default="vec![]")] 29 | id: StateID, 30 | #[builder(setter(into))] 31 | label: StateLabel, 32 | #[builder(default="vec![]")] 33 | on_entry: Vec, 34 | #[builder(default="vec![]")] 35 | on_exit: Vec, 36 | #[builder(default="vec![]")] 37 | transitions: Vec, 38 | #[builder(setter(skip))] 39 | parent: Option, 40 | } 41 | 42 | impl PartialEq for Atomic { 43 | fn eq(&self, other: &Self) -> bool { 44 | (&self.id, &self.label, &self.on_entry, &self.on_exit, &self.transitions) == 45 | (&other.id, &other.label, &other.on_entry, &other.on_exit, &other.transitions) 46 | } 47 | } 48 | 49 | impl Node for Atomic { 50 | fn id(&self) -> &StateID { 51 | &self.id 52 | } 53 | fn label(&self) -> &StateLabel { 54 | &self.label 55 | } 56 | fn substate(&self, _: &str) -> Option<&State> { 57 | None 58 | } 59 | fn initial(&self) -> Option<&StateLabel> { 60 | None 61 | } 62 | fn parent(&self) -> Option<&StateID> { 63 | self.parent.as_ref() 64 | } 65 | fn on_entry(&self) -> &Vec { 66 | &self.on_entry 67 | } 68 | fn on_exit(&self) -> &Vec { 69 | &self.on_exit 70 | } 71 | } 72 | 73 | impl ActiveNode for Atomic { 74 | fn transitions(&self) -> &Vec { 75 | &self.transitions 76 | } 77 | } 78 | 79 | impl Atomic { 80 | pub fn node(&self) -> &Node { 81 | self as &Node 82 | } 83 | pub fn active_node(&self) -> &ActiveNode { 84 | self as &ActiveNode 85 | } 86 | } 87 | 88 | #[derive(Debug, Clone, Builder)] 89 | pub struct Compound { 90 | #[builder(default="vec![]")] 91 | id: StateID, 92 | #[builder(setter(into))] 93 | label: StateLabel, 94 | #[builder(default="None")] 95 | initial_label: Option, 96 | #[builder(default="vec![]")] 97 | on_entry: Vec, 98 | #[builder(default="vec![]")] 99 | on_exit: Vec, 100 | #[builder(default="vec![]")] 101 | transitions: Vec, 102 | #[builder(default="vec![]")] 103 | substates: Vec, 104 | #[builder(setter(skip))] 105 | parent: Option, 106 | } 107 | 108 | impl PartialEq for Compound { 109 | fn eq(&self, other: &Self) -> bool { 110 | (&self.id, 111 | &self.label, 112 | &self.initial_label, 113 | &self.on_entry, 114 | &self.on_exit, 115 | &self.transitions, 116 | &self.substates) == 117 | (&other.id, 118 | &other.label, 119 | &other.initial_label, 120 | &other.on_entry, 121 | &other.on_exit, 122 | &other.transitions, 123 | &other.substates) 124 | } 125 | } 126 | 127 | impl Node for Compound { 128 | fn id(&self) -> &StateID { 129 | &self.id 130 | } 131 | fn label(&self) -> &StateLabel { 132 | &self.label 133 | } 134 | fn substate(&self, label: &str) -> Option<&State> { 135 | for ss in self.substates.iter() { 136 | if ss.node().label() == label { 137 | return Some(ss); 138 | } 139 | } 140 | None 141 | } 142 | fn initial(&self) -> Option<&StateLabel> { 143 | match self.initial_label { 144 | Some(ref l) => Some(l), 145 | None => { 146 | for ss in self.substates.iter() { 147 | return Some(ss.node().label()); 148 | } 149 | None 150 | } 151 | } 152 | } 153 | fn parent(&self) -> Option<&StateID> { 154 | self.parent.as_ref() 155 | } 156 | fn on_entry(&self) -> &Vec { 157 | &self.on_entry 158 | } 159 | fn on_exit(&self) -> &Vec { 160 | &self.on_exit 161 | } 162 | } 163 | impl ActiveNode for Compound { 164 | fn transitions(&self) -> &Vec { 165 | &self.transitions 166 | } 167 | } 168 | 169 | impl Compound { 170 | pub fn node(&self) -> &Node { 171 | self as &Node 172 | } 173 | pub fn active_node(&self) -> &ActiveNode { 174 | self as &ActiveNode 175 | } 176 | } 177 | 178 | #[derive(Debug, Clone, Builder)] 179 | pub struct Parallel { 180 | #[builder(default="vec![]")] 181 | id: StateID, 182 | #[builder(setter(into))] 183 | label: StateLabel, 184 | #[builder(default="vec![]")] 185 | on_entry: Vec, 186 | #[builder(default="vec![]")] 187 | on_exit: Vec, 188 | #[builder(default="vec![]")] 189 | transitions: Vec, 190 | #[builder(default="vec![]")] 191 | substates: Vec, 192 | #[builder(setter(skip))] 193 | parent: Option, 194 | } 195 | 196 | impl PartialEq for Parallel { 197 | fn eq(&self, other: &Self) -> bool { 198 | (&self.id, 199 | &self.label, 200 | &self.on_entry, 201 | &self.on_exit, 202 | &self.transitions, 203 | &self.substates) == 204 | (&other.id, 205 | &other.label, 206 | &other.on_entry, 207 | &other.on_exit, 208 | &other.transitions, 209 | &other.substates) 210 | } 211 | } 212 | 213 | impl Node for Parallel { 214 | fn id(&self) -> &StateID { 215 | &self.id 216 | } 217 | fn label(&self) -> &StateLabel { 218 | &self.label 219 | } 220 | fn substate(&self, label: &str) -> Option<&State> { 221 | for ss in self.substates.iter() { 222 | if ss.node().label() == label { 223 | return Some(ss); 224 | } 225 | } 226 | None 227 | } 228 | fn initial(&self) -> Option<&StateLabel> { 229 | None 230 | } 231 | fn parent(&self) -> Option<&StateID> { 232 | self.parent.as_ref() 233 | } 234 | fn on_entry(&self) -> &Vec { 235 | &self.on_entry 236 | } 237 | fn on_exit(&self) -> &Vec { 238 | &self.on_exit 239 | } 240 | } 241 | impl ActiveNode for Parallel { 242 | fn transitions(&self) -> &Vec { 243 | &self.transitions 244 | } 245 | } 246 | 247 | impl Parallel { 248 | pub fn node(&self) -> &Node { 249 | self as &Node 250 | } 251 | pub fn active_node(&self) -> &ActiveNode { 252 | self as &ActiveNode 253 | } 254 | } 255 | 256 | #[derive(Debug, Clone, Builder)] 257 | pub struct Final { 258 | #[builder(default="vec![]")] 259 | id: StateID, 260 | #[builder(setter(into))] 261 | label: StateLabel, 262 | #[builder(default="vec![]")] 263 | on_entry: Vec, 264 | #[builder(default="vec![]")] 265 | on_exit: Vec, 266 | #[builder(default="Output::Empty(Empty)")] 267 | result: Output, 268 | #[builder(setter(skip))] 269 | parent: Option, 270 | } 271 | 272 | impl PartialEq for Final { 273 | fn eq(&self, other: &Self) -> bool { 274 | (&self.id, &self.label, &self.on_entry, &self.on_exit, &self.result) == 275 | (&other.id, &other.label, &other.on_entry, &other.on_exit, &other.result) 276 | } 277 | } 278 | 279 | impl Node for Final { 280 | fn id(&self) -> &StateID { 281 | &self.id 282 | } 283 | fn label(&self) -> &StateLabel { 284 | &self.label 285 | } 286 | fn substate(&self, _: &str) -> Option<&State> { 287 | None 288 | } 289 | fn initial(&self) -> Option<&StateLabel> { 290 | None 291 | } 292 | fn parent(&self) -> Option<&StateID> { 293 | self.parent.as_ref() 294 | } 295 | fn on_entry(&self) -> &Vec { 296 | &self.on_entry 297 | } 298 | fn on_exit(&self) -> &Vec { 299 | &self.on_exit 300 | } 301 | } 302 | 303 | impl Final { 304 | pub fn node(&self) -> &Node { 305 | self as &Node 306 | } 307 | pub fn result(&self) -> &Output { 308 | &self.result 309 | } 310 | } 311 | 312 | #[derive(Debug, PartialEq, Clone)] 313 | pub enum State { 314 | Atomic(Atomic), 315 | Compound(Compound), 316 | Parallel(Parallel), 317 | Final(Final), 318 | } 319 | 320 | impl State { 321 | pub fn node(&self) -> &Node { 322 | match self { 323 | &State::Atomic(ref a) => a.node(), 324 | &State::Compound(ref c) => c.node(), 325 | &State::Parallel(ref p) => p.node(), 326 | &State::Final(ref f) => f.node(), 327 | } 328 | } 329 | pub fn active_node(&self) -> Option<&ActiveNode> { 330 | match self { 331 | &State::Atomic(ref a) => Some(a.active_node()), 332 | &State::Compound(ref c) => Some(c.active_node()), 333 | &State::Parallel(ref p) => Some(p.active_node()), 334 | &State::Final(_) => None, 335 | } 336 | } 337 | fn find(&self, id: &StateID) -> Option<&State> { 338 | self.find_from(id, 0) 339 | } 340 | fn find_from(&self, id: &StateID, depth: usize) -> Option<&State> { 341 | if depth == id.len() { 342 | return Some(self); 343 | } 344 | let ss = match self { 345 | &State::Atomic(_) => return None, 346 | &State::Compound(ref c) => &c.substates, 347 | &State::Parallel(ref p) => &p.substates, 348 | &State::Final(_) => return None, 349 | }; 350 | if id[depth] < ss.len() { 351 | ss[id[depth]].find_from(id, depth + 1) 352 | } else { 353 | None 354 | } 355 | } 356 | pub fn allstates(st: &State) -> Vec<&State> { 357 | iter::once(st).chain(st.substates().flat_map(|ss| State::allstates(ss))).collect() 358 | } 359 | pub fn substates(&self) -> Iter { 360 | match self { 361 | &State::Atomic(_) => [].iter(), 362 | &State::Compound(ref c) => c.substates.iter(), 363 | &State::Parallel(ref p) => p.substates.iter(), 364 | &State::Final(_) => [].iter(), 365 | } 366 | } 367 | fn substates_mut(&mut self) -> IterMut { 368 | match self { 369 | &mut State::Atomic(_) => [].iter_mut(), 370 | &mut State::Compound(ref mut c) => c.substates.iter_mut(), 371 | &mut State::Parallel(ref mut p) => p.substates.iter_mut(), 372 | &mut State::Final(_) => [].iter_mut(), 373 | } 374 | } 375 | fn set_parent(&mut self, parent: StateID) { 376 | match self { 377 | &mut State::Atomic(ref mut a) => a.parent = Some(parent), 378 | &mut State::Compound(ref mut c) => c.parent = Some(parent), 379 | &mut State::Parallel(ref mut p) => p.parent = Some(parent), 380 | &mut State::Final(ref mut f) => f.parent = Some(parent), 381 | } 382 | } 383 | fn init(st: &mut State, id: StateID) { 384 | { 385 | match st { 386 | &mut State::Atomic(ref mut a) => { 387 | a.id = id; 388 | return; 389 | } 390 | &mut State::Final(ref mut f) => { 391 | f.id = id; 392 | return; 393 | } 394 | &mut State::Compound(ref mut c) => c.id = id.clone(), 395 | &mut State::Parallel(ref mut p) => p.id = id.clone(), 396 | } 397 | } 398 | let mut i = 0; 399 | for ss in st.substates_mut() { 400 | let mut child_id = id.clone(); 401 | child_id.push(i); 402 | ss.set_parent(id.clone()); 403 | State::init(ss, child_id); 404 | i += 1; 405 | } 406 | } 407 | } 408 | 409 | #[derive(Debug, PartialEq, Clone, Builder)] 410 | pub struct Transition { 411 | #[builder(default="HashSet::new()")] 412 | pub topics: HashSet, 413 | #[builder(default="Condition::True(True)")] 414 | pub cond: Condition, 415 | #[builder(default="vec![]")] 416 | pub actions: Vec, 417 | #[builder(default="None")] 418 | pub target_label: Option, 419 | } 420 | 421 | pub struct Context { 422 | root: State, 423 | state_by_label: HashMap, 424 | } 425 | 426 | impl Context { 427 | pub fn new(root: State) -> Context { 428 | let mut root = root; 429 | State::init(&mut root, vec![]); 430 | let root = root; 431 | let mut ctx = Context { 432 | root: root, 433 | state_by_label: HashMap::new(), 434 | }; 435 | for st in State::allstates(&ctx.root) { 436 | let n = st.node(); 437 | ctx.state_by_label.insert(n.label().to_owned(), n.id().clone()); 438 | } 439 | ctx 440 | } 441 | pub fn root(&self) -> &State { 442 | &self.root 443 | } 444 | pub fn state(&self, label: &str) -> Option<&State> { 445 | match self.state_by_label.get(label) { 446 | Some(ref id) => self.state_by_id(id), 447 | None => None, 448 | } 449 | } 450 | pub fn state_by_id(&self, id: &StateID) -> Option<&State> { 451 | self.root.find(id) 452 | } 453 | } 454 | 455 | #[macro_export] 456 | macro_rules! state { 457 | ($label:ident {$($tail:tt)*}) => {{ 458 | let mut stb = $crate::ast::AtomicBuilder::default(); 459 | stb.label(stringify!($label)); 460 | state_props!(stb {$($tail)*}); 461 | $crate::ast::State::Atomic(stb.build().unwrap()) 462 | }} 463 | } 464 | 465 | #[macro_export] 466 | macro_rules! states { 467 | ($label:ident {$($tail:tt)*}) => {{ 468 | let mut stb = $crate::ast::CompoundBuilder::default(); 469 | stb.label(stringify!($label)); 470 | state_props!(stb {$($tail)*}); 471 | $crate::ast::State::Compound(stb.build().unwrap()) 472 | }} 473 | } 474 | 475 | #[macro_export] 476 | macro_rules! parallel { 477 | ($label:ident {$($tail:tt)*}) => {{ 478 | let mut stb = $crate::ast::ParallelBuilder::default(); 479 | stb.label(stringify!($label)); 480 | state_props!(stb {$($tail)*}); 481 | $crate::ast::State::Parallel(stb.build().unwrap()) 482 | }} 483 | } 484 | 485 | #[macro_export] 486 | macro_rules! final_state { 487 | ($label:ident {$($tail:tt)*}) => {{ 488 | let mut stb = $crate::ast::FinalBuilder::default(); 489 | stb.label(stringify!($label)); 490 | state_props!(stb {$($tail)*}); 491 | $crate::ast::State::Final(stb.build().unwrap()) 492 | }} 493 | } 494 | 495 | #[macro_export] 496 | macro_rules! state_props { 497 | ($stb:ident {$key:ident: $value:expr}) => { 498 | state_prop!($stb, $key, $value); 499 | }; 500 | ($stb:ident {$key:ident: $value:expr, $($tail:tt)*}) => { 501 | state_prop!($stb, $key, $value); 502 | state_props!($stb {$($tail)*}); 503 | }; 504 | ($stb:ident {}) => {} 505 | } 506 | 507 | #[macro_export] 508 | macro_rules! state_prop { 509 | ($stb:ident, substates, $value:expr) => { 510 | $stb.substates($value.iter().cloned().collect()); 511 | }; 512 | ($stb:ident, on_entry, $value:expr) => { 513 | $stb.on_entry($value.iter().cloned().collect()); 514 | }; 515 | ($stb:ident, on_exit, $value:expr) => { 516 | $stb.on_exit($value.iter().cloned().collect()); 517 | }; 518 | ($stb:ident, transitions, $value:expr) => { 519 | $stb.transitions($value.iter().cloned().collect()); 520 | }; 521 | ($stb:ident, $key:ident, $value:expr) => { 522 | $stb.$key($value); 523 | } 524 | } 525 | 526 | #[macro_export] 527 | macro_rules! goto { 528 | (@props $t:ident, $key:ident: $value:expr) => { 529 | goto!{@prop $t, $key, $value}; 530 | }; 531 | (@props $t:ident, $key:ident: $value:expr, $($tail:tt)*) => { 532 | goto!{@prop $t, $key, $value}; 533 | goto!{@props $t, $($tail)*}; 534 | }; 535 | (@prop $t:ident, target, $value:ident) => { 536 | $t.target_label(Some(stringify!($value).to_owned())); 537 | }; 538 | (@prop $t:ident, topics, $value:expr) => { 539 | $t.topics($value.iter().map(|x|{x.to_string()}).collect()); 540 | }; 541 | (@prop $t:ident, actions, $value:expr) => { 542 | $t.actions($value.iter().cloned().collect()); 543 | }; 544 | (@prop $t:ident, $key:ident, $value:expr) => { 545 | $t.$key($value); 546 | }; 547 | ($key:ident: $value:tt) => {{ 548 | let mut t = $crate::ast::TransitionBuilder::default(); 549 | goto!{@prop t, $key, $value}; 550 | t.build().unwrap() 551 | }}; 552 | ($key:ident: $value:tt, $($tail:tt)*) => {{ 553 | let mut t = $crate::ast::TransitionBuilder::default(); 554 | goto!{@prop t, $key, $value}; 555 | goto!{@props t, $($tail)*}; 556 | t.build().unwrap() 557 | }}; 558 | } 559 | -------------------------------------------------------------------------------- /src/interpreter.rs: -------------------------------------------------------------------------------- 1 | use std::cmp::{max, Ordering}; 2 | use std::collections::{HashMap, HashSet}; 3 | use std::fmt; 4 | use std::rc::Rc; 5 | 6 | use chrono; 7 | 8 | use ast::*; 9 | 10 | pub struct Interpreter { 11 | vars: Object, 12 | events: EventQueue, 13 | status: Status, 14 | current_event: Option, 15 | current_config: Vec, 16 | next_config: Vec, 17 | exiting_parallels: HashSet, 18 | sender: Box, 19 | } 20 | 21 | impl Interpreter { 22 | pub fn new() -> Interpreter { 23 | Interpreter { 24 | vars: Object::new(), 25 | events: EventQueue::new(), 26 | status: Status::New, 27 | current_event: None, 28 | current_config: vec![], 29 | next_config: vec![], 30 | exiting_parallels: HashSet::new(), 31 | sender: Box::new(NopSender), 32 | } 33 | } 34 | pub fn get_var(&self, key: &str) -> Option<&Value> { 35 | self.vars.get(key) 36 | } 37 | pub fn set_var(&mut self, key: &str, value: Value) { 38 | self.vars.insert(key.to_owned(), value); 39 | } 40 | pub fn push_event(&mut self, ev: Event) { 41 | self.events.push(ev) 42 | } 43 | pub fn run(&mut self, ctx: &Context) -> Result { 44 | loop { 45 | trace!("BEGIN macrostep"); 46 | self.status = self.macrostep(ctx)?; 47 | trace!("END macrostep: status={:?}", self.status); 48 | match self.status { 49 | Status::Done(ref o) => return Ok(o.clone()), 50 | Status::Blocked => return Err(Fault::BlockedIndefinitely), 51 | _ => {} 52 | } 53 | } 54 | } 55 | pub fn step(&mut self, ctx: &Context) -> Result { 56 | trace!("BEGIN macrostep"); 57 | self.status = self.macrostep(ctx)?; 58 | trace!("END macrostep: status={:?}", self.status); 59 | Ok(self.status.clone()) 60 | } 61 | fn macrostep(&mut self, ctx: &Context) -> Result { 62 | match self.status { 63 | Status::New => { 64 | let start_t = TransitionBuilder::default() 65 | .target_label(Some(ctx.root().node().label().clone())) 66 | .build() 67 | .unwrap(); 68 | return match self.microstep(ctx, ctx.root(), &start_t) { 69 | Ok(status) => { 70 | self.complete_macrostep(); 71 | Ok(status) 72 | } 73 | Err(e) => Err(e), 74 | }; 75 | } 76 | Status::Done(_) => return Ok(self.status.clone()), 77 | _ => {} 78 | } 79 | 80 | let mut results = vec![]; 81 | 82 | // First follow all eventless transitions that can be taken 83 | for i in 0..self.current_config.len() { 84 | let st = match ctx.state_by_id(&self.current_config[i]) { 85 | None => return Err(Fault::CurrentStateUndefined), 86 | ref sst => sst.unwrap(), 87 | }; 88 | if let &State::Final(ref f) = st { 89 | for on_exit in f.on_exit() { 90 | on_exit.actionable().apply(self)?; 91 | } 92 | results.push(Status::Done(f.result().outputable().eval(self))); 93 | continue; 94 | } 95 | match self.eventless_transition(ctx, st) { 96 | Ok(Some(status)) => { 97 | results.push(status); 98 | continue; 99 | } 100 | Ok(None) => {} 101 | Err(e) => return Err(e), 102 | } 103 | } 104 | if !results.is_empty() { 105 | self.complete_macrostep(); 106 | return Ok(results.into_iter().fold(Status::Blocked, |agg, val| max(agg, val))); 107 | } 108 | 109 | // Then follow transitions matched by the next event in the queue. 110 | if let Some(ref ev) = self.events.pop() { 111 | for i in 0..self.current_config.len() { 112 | let st = match ctx.state_by_id(&self.current_config[i]) { 113 | None => return Err(Fault::CurrentStateUndefined), 114 | ref sst => sst.unwrap(), 115 | }; 116 | match self.event_transition(ctx, st, ev) { 117 | Ok(Some(status)) => { 118 | results.push(status); 119 | continue; 120 | } 121 | Ok(None) => {} 122 | Err(e) => return Err(e), 123 | } 124 | } 125 | } 126 | if !results.is_empty() { 127 | self.complete_macrostep(); 128 | return Ok(results.into_iter().fold(Status::Blocked, |agg, val| max(agg, val))); 129 | } 130 | 131 | // Finally, enter any non-atomic states in the current configuration. 132 | for i in 0..self.current_config.len() { 133 | let st = match ctx.state_by_id(&self.current_config[i]) { 134 | None => return Err(Fault::CurrentStateUndefined), 135 | ref sst => sst.unwrap(), 136 | }; 137 | if let Some(label) = st.node().initial() { 138 | let status = self.microstep(ctx, 139 | st, 140 | &TransitionBuilder::default() 141 | .target_label(Some(label.to_owned())) 142 | .build() 143 | .unwrap())?; 144 | results.push(status); 145 | } else if let &State::Parallel(_) = st { 146 | for sub_st in st.substates() { 147 | let status = self.microstep(ctx, 148 | st, 149 | &TransitionBuilder::default() 150 | .target_label(Some(sub_st.node().label().to_owned())) 151 | .build() 152 | .unwrap())?; 153 | results.push(status); 154 | } 155 | } 156 | } 157 | self.complete_macrostep(); 158 | Ok(results.into_iter().fold(Status::Blocked, |agg, val| max(agg, val))) 159 | } 160 | fn complete_macrostep(&mut self) { 161 | self.current_config.clear(); 162 | self.current_config.append(&mut self.next_config); 163 | self.exiting_parallels.clear(); 164 | } 165 | fn eventless_transition(&mut self, ctx: &Context, st: &State) -> Result, Fault> { 166 | let mut sst = Some(st); 167 | loop { 168 | sst = match sst { 169 | Some(st) => { 170 | if let Some(n) = st.active_node() { 171 | for t in n.transitions() { 172 | if t.topics.is_empty() && t.cond.conditional().eval(self) { 173 | let status = self.microstep(ctx, st, t)?; 174 | return Ok(Some(status)); 175 | } 176 | } 177 | match n.parent() { 178 | None => None, 179 | Some(ref p) => ctx.state_by_id(p), 180 | } 181 | } else { 182 | None 183 | } 184 | } 185 | None => return Ok(None), 186 | }; 187 | } 188 | } 189 | fn event_transition(&mut self, 190 | ctx: &Context, 191 | st: &State, 192 | ev: &Event) 193 | -> Result, Fault> { 194 | let mut sst = Some(st); 195 | loop { 196 | sst = match sst { 197 | Some(st) => { 198 | if let Some(n) = st.active_node() { 199 | for t in n.transitions() { 200 | if t.topics.contains(&ev.topic) && t.cond.conditional().eval(self) { 201 | let status = self.microstep(ctx, st, t)?; 202 | return Ok(Some(status)); 203 | } 204 | } 205 | match n.parent() { 206 | None => None, 207 | Some(ref p) => ctx.state_by_id(p), 208 | } 209 | } else { 210 | None 211 | } 212 | } 213 | None => return Ok(None), 214 | }; 215 | } 216 | } 217 | fn microstep(&mut self, 218 | ctx: &Context, 219 | current_st: &State, 220 | t: &Transition) 221 | -> Result { 222 | trace!("microstep: {:?}", t); 223 | match t.target_label { 224 | None => { 225 | // Execute actions in the current transition and that's it. 226 | for i in 0..t.actions.len() { 227 | t.actions[i].actionable().apply(self)?; 228 | } 229 | Ok(Status::Runnable) 230 | } 231 | Some(ref target_label) => { 232 | let target_st = match ctx.state(target_label) { 233 | Some(r) => r, 234 | None => return Err(Fault::LabelNotFound(target_label.clone())), 235 | }; 236 | let current_id = current_st.node().id().clone(); 237 | // Execute on_exit for all states we are leaving in this transition. 238 | let exit_states = Interpreter::exit_states(¤t_id, target_st.node().id()); 239 | for id in exit_states { 240 | let exit_state = match ctx.state_by_id(&id) { 241 | Some(st) => st, 242 | None => return Err(Fault::IDNotFound(id.clone())), 243 | }; 244 | if let State::Parallel(_) = *exit_state { 245 | if self.exiting_parallels.contains(&id) { 246 | // If we've already exited this parallel (in a prior parallel 247 | // microstep), we're done. 248 | return Ok(Status::TerminatedParallel); 249 | } else { 250 | self.exiting_parallels.insert(id.clone()); 251 | } 252 | } 253 | for on_exit in exit_state.node().on_exit() { 254 | on_exit.actionable().apply(self)?; 255 | } 256 | } 257 | // Execute actions in the current transition. 258 | for i in 0..t.actions.len() { 259 | t.actions[i].actionable().apply(self)?; 260 | } 261 | // Execute on_entry for all states we are entering in this transition. 262 | let entry_states = Interpreter::entry_states(¤t_id, target_st.node().id()); 263 | for id in entry_states { 264 | let entry_state = match ctx.state_by_id(&id) { 265 | Some(st) => st, 266 | None => return Err(Fault::IDNotFound(id.clone())), 267 | }; 268 | for on_entry in entry_state.node().on_entry() { 269 | on_entry.actionable().apply(self)?; 270 | } 271 | } 272 | self.next_config.push(target_st.node().id().clone()); 273 | Ok(Status::Runnable) 274 | } 275 | } 276 | } 277 | pub fn common_ancestor(from: &StateID, to: &StateID) -> StateID { 278 | let mut common = vec![]; 279 | for i in 0..from.len() { 280 | if i >= to.len() { 281 | break; 282 | } 283 | if from[i] == to[i] { 284 | common.push(from[i]); 285 | } else { 286 | break; 287 | } 288 | } 289 | common 290 | } 291 | pub fn exit_states(from: &StateID, to: &StateID) -> Vec { 292 | let common = Interpreter::common_ancestor(from, to); 293 | let mut result = vec![]; 294 | let mut i = from.len(); 295 | while i > common.len() { 296 | let id = from[0..i].to_vec(); 297 | result.push(id); 298 | i = i - 1; 299 | } 300 | result 301 | } 302 | pub fn entry_states(from: &StateID, to: &StateID) -> Vec { 303 | let common = Interpreter::common_ancestor(from, to); 304 | let mut result = vec![]; 305 | for i in common.len()..to.len() { 306 | let id = to[0..i + 1].to_vec(); 307 | result.push(id); 308 | } 309 | result 310 | } 311 | } 312 | 313 | #[derive(Debug, PartialEq, Clone)] 314 | pub enum Status { 315 | New, 316 | Runnable, 317 | Blocked, 318 | TerminatedParallel, 319 | Done(Value), 320 | } 321 | 322 | impl Ord for Status { 323 | fn cmp(&self, other: &Status) -> Ordering { 324 | self.to_i32().cmp(&other.to_i32()) 325 | } 326 | } 327 | 328 | impl PartialOrd for Status { 329 | fn partial_cmp(&self, other: &Status) -> Option { 330 | Some(self.cmp(other)) 331 | } 332 | } 333 | 334 | impl Eq for Status {} 335 | 336 | impl Status { 337 | fn to_i32(&self) -> i32 { 338 | match self { 339 | &Status::New => 1, 340 | &Status::Runnable => 2, 341 | &Status::Blocked => 0, 342 | &Status::TerminatedParallel => -1, 343 | &Status::Done(_) => 3, 344 | } 345 | } 346 | } 347 | 348 | #[derive(Debug, PartialEq)] 349 | pub enum Fault { 350 | LabelNotFound(StateLabel), 351 | IDNotFound(StateID), 352 | CurrentStateUndefined, 353 | ActionError(String), 354 | BlockedIndefinitely, 355 | UndeliverableSenderTarget(String), 356 | } 357 | 358 | pub trait Actionable { 359 | fn apply(&self, &mut Interpreter) -> Result<(), Fault>; 360 | } 361 | 362 | #[derive(Debug, PartialEq, Clone, Builder)] 363 | pub struct Log { 364 | #[builder(default, setter(into))] 365 | label: String, 366 | #[builder(default, setter(into))] 367 | message: String, 368 | } 369 | 370 | #[macro_export] 371 | macro_rules! action_log { 372 | ($($key:ident: $value:expr),*) => { 373 | $crate::interpreter::Action::Log($crate::interpreter::LogBuilder::default()$(.$key($value))*.build().unwrap()) 374 | } 375 | } 376 | 377 | impl Actionable for Log { 378 | fn apply(&self, _: &mut Interpreter) -> Result<(), Fault> { 379 | info!("[{}]{}: {}", 380 | chrono::prelude::Utc::now().format("%Y-%m-%d %H:%M:%S"), 381 | self.label, 382 | self.message); 383 | Ok(()) 384 | } 385 | } 386 | 387 | #[derive(Debug, PartialEq, Clone, Builder)] 388 | pub struct Raise { 389 | #[builder(setter(into))] 390 | topic: String, 391 | #[builder(default="Value::Object(HashMap::new())")] 392 | contents: Value, 393 | } 394 | 395 | #[macro_export] 396 | macro_rules! action_raise { 397 | ($($key:ident: $value:expr),*) => { 398 | $crate::interpreter::Action::Raise($crate::interpreter::RaiseBuilder::default()$(.$key($value))*.build().unwrap()) 399 | } 400 | } 401 | 402 | impl Actionable for Raise { 403 | fn apply(&self, it: &mut Interpreter) -> Result<(), Fault> { 404 | trace!("raise {:?}", self); 405 | it.events.push(Event { 406 | topic: self.topic.clone(), 407 | contents: self.contents.clone(), 408 | }); 409 | Ok(()) 410 | } 411 | } 412 | 413 | #[derive(Debug, PartialEq, Clone, Builder)] 414 | pub struct Assign { 415 | #[builder(setter(into))] 416 | key: String, 417 | // TODO: make this an Expression 418 | value: Value, 419 | } 420 | 421 | #[macro_export] 422 | macro_rules! action_assign { 423 | ($($key:ident: $value:expr),*) => { 424 | $crate::interpreter::Action::Assign($crate::interpreter::AssignBuilder::default()$(.$key($value))*.build().unwrap()) 425 | } 426 | } 427 | 428 | impl Actionable for Assign { 429 | fn apply(&self, it: &mut Interpreter) -> Result<(), Fault> { 430 | // TODO: partial assignment into objects & lists 431 | it.vars.insert(self.key.to_owned(), self.value.clone()); 432 | Ok(()) 433 | } 434 | } 435 | 436 | #[derive(Debug, PartialEq, Clone, Builder)] 437 | pub struct Choose { 438 | #[builder(default="vec![]")] 439 | when: Vec<(Condition, Box)>, 440 | #[builder(default="None")] 441 | otherwise: Option>, 442 | } 443 | 444 | impl Actionable for Choose { 445 | fn apply(&self, it: &mut Interpreter) -> Result<(), Fault> { 446 | for &(ref cond, ref action) in &self.when { 447 | if cond.conditional().eval(it) { 448 | return action.actionable().apply(it); 449 | } 450 | } 451 | if let &Some(ref action) = &self.otherwise { 452 | return action.actionable().apply(it); 453 | } 454 | Ok(()) 455 | } 456 | } 457 | 458 | #[macro_export] 459 | macro_rules! choose { 460 | (@props $c:ident) => {}; 461 | (@props $c:ident, $key:ident: $value:expr) => { 462 | choose!{@prop $c, $key, $value}; 463 | }; 464 | (@props $c:ident, $key:ident: $value:expr, $($tail:tt)*) => { 465 | choose!{@prop $c, $key, $value}; 466 | choose!{@props $c $($tail)*}; 467 | }; 468 | (@prop $c:ident, otherwise, $value:expr) => { 469 | $c.otherwise(Some(Box::new($value))); 470 | }; 471 | (@prop $c:ident, when, $conds:expr) => { 472 | $c.when($conds.iter().cloned().map(|(c, a)|{(c, Box::new(a))}).collect()); 473 | }; 474 | ($key:ident: $value:tt) => {{ 475 | let mut c = $crate::interpreter::ChooseBuilder::default(); 476 | choose!{@prop c, $key, $value}; 477 | $crate::interpreter::Action::Choose(c.build().unwrap()) 478 | }}; 479 | ($key:ident: $value:tt, $($tail:tt)*) => {{ 480 | let mut c = $crate::interpreter::ChooseBuilder::default(); 481 | choose!{@prop c, $key, $value}; 482 | choose!{@props c, $($tail)*}; 483 | $crate::interpreter::Action::Choose(c.build().unwrap()) 484 | }}; 485 | } 486 | 487 | #[derive(Clone)] 488 | pub struct ActionFn { 489 | f: Rc Result<(), Fault>>, 490 | } 491 | 492 | #[macro_export] 493 | macro_rules! action_fn { 494 | ($f:expr) => { 495 | $crate::interpreter::Action::Fn($crate::interpreter::ActionFn::new($f)) 496 | } 497 | } 498 | 499 | impl fmt::Debug for ActionFn { 500 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 501 | write!(f, "(ActionFn)") 502 | } 503 | } 504 | 505 | impl PartialEq for ActionFn { 506 | fn eq(&self, _: &Self) -> bool { 507 | return false; 508 | } 509 | } 510 | 511 | impl Actionable for ActionFn { 512 | fn apply(&self, it: &mut Interpreter) -> Result<(), Fault> { 513 | (self.f)(it) 514 | } 515 | } 516 | 517 | impl ActionFn { 518 | pub fn new(f: Rc Result<(), Fault>>) -> ActionFn { 519 | ActionFn { f: f } 520 | } 521 | } 522 | 523 | #[derive(Debug, PartialEq, Clone)] 524 | pub enum Action { 525 | Log(Log), 526 | Raise(Raise), 527 | Assign(Assign), 528 | Choose(Choose), 529 | Fn(ActionFn), 530 | } 531 | 532 | impl Action { 533 | fn actionable(&self) -> &Actionable { 534 | match self { 535 | &Action::Log(ref a) => a, 536 | &Action::Raise(ref s) => s, 537 | &Action::Assign(ref a) => a, 538 | &Action::Choose(ref c) => c, 539 | &Action::Fn(ref f) => f, 540 | } 541 | } 542 | } 543 | 544 | pub trait Conditional { 545 | fn eval(&self, &Interpreter) -> bool; 546 | } 547 | 548 | #[derive(Debug, PartialEq, Clone)] 549 | pub struct True; 550 | 551 | impl Conditional for True { 552 | fn eval(&self, _: &Interpreter) -> bool { 553 | true 554 | } 555 | } 556 | 557 | #[derive(Clone)] 558 | pub struct CondFn { 559 | f: Rc bool>, 560 | } 561 | 562 | impl fmt::Debug for CondFn { 563 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 564 | write!(f, "(CondFn)") 565 | } 566 | } 567 | 568 | impl PartialEq for CondFn { 569 | fn eq(&self, _: &Self) -> bool { 570 | return false; 571 | } 572 | } 573 | 574 | impl Conditional for CondFn { 575 | fn eval(&self, it: &Interpreter) -> bool { 576 | (self.f)(it) 577 | } 578 | } 579 | 580 | impl CondFn { 581 | pub fn new(f: Rc bool>) -> CondFn { 582 | CondFn { f: f } 583 | } 584 | } 585 | 586 | #[macro_export] 587 | macro_rules! cond_fn { 588 | ($f:expr) => { 589 | $crate::interpreter::Condition::Fn($crate::interpreter::CondFn::new(Rc::new($f))) 590 | } 591 | } 592 | 593 | #[derive(Debug, PartialEq, Clone)] 594 | pub enum Condition { 595 | True(True), 596 | Fn(CondFn), /* TODO: 597 | * Expression(Expression), */ 598 | } 599 | 600 | impl Condition { 601 | pub fn conditional(&self) -> &Conditional { 602 | match self { 603 | &Condition::True(ref c) => c, 604 | &Condition::Fn(ref f) => f, 605 | } 606 | } 607 | } 608 | 609 | pub trait Outputable { 610 | fn eval(&self, &Interpreter) -> Value; 611 | } 612 | 613 | #[derive(Debug, PartialEq, Clone)] 614 | pub struct Empty; 615 | 616 | impl Outputable for Empty { 617 | fn eval(&self, _: &Interpreter) -> Value { 618 | Value::Object(HashMap::new()) 619 | } 620 | } 621 | 622 | #[derive(Debug, PartialEq, Clone)] 623 | pub enum Output { 624 | Empty(Empty), 625 | ValueOf(ValueOf), 626 | } 627 | 628 | #[derive(Debug, PartialEq, Clone, Builder)] 629 | pub struct ValueOf { 630 | #[builder(setter(into))] 631 | key: String, 632 | } 633 | 634 | impl Outputable for ValueOf { 635 | fn eval(&self, it: &Interpreter) -> Value { 636 | match it.vars.get(&self.key) { 637 | Some(v) => v.clone(), 638 | None => Value::None, 639 | } 640 | } 641 | } 642 | 643 | impl Output { 644 | pub fn outputable(&self) -> &Outputable { 645 | match self { 646 | &Output::Empty(ref o) => o, 647 | &Output::ValueOf(ref v) => v, 648 | } 649 | } 650 | } 651 | 652 | pub type Object = HashMap; 653 | 654 | #[derive(Debug, PartialEq, Clone)] 655 | pub enum Value { 656 | Bool(bool), 657 | Int(i32), 658 | String(String), 659 | List(Vec), 660 | Object(HashMap), 661 | None, 662 | } 663 | 664 | impl Value { 665 | pub fn from_str(s: S) -> Value 666 | where S: Into 667 | { 668 | Value::String(s.into()) 669 | } 670 | } 671 | 672 | #[derive(Debug, PartialEq, Clone)] 673 | pub struct Event { 674 | topic: String, 675 | contents: Value, 676 | } 677 | 678 | #[derive(Debug)] 679 | pub struct EventQueue(Vec); 680 | 681 | impl EventQueue { 682 | pub fn new() -> EventQueue { 683 | EventQueue(vec![]) 684 | } 685 | pub fn push(&mut self, ev: Event) { 686 | self.0.insert(0, ev) 687 | } 688 | pub fn pop(&mut self) -> Option { 689 | self.0.pop() 690 | } 691 | } 692 | 693 | pub trait Sender { 694 | fn send(&self, target: &str, ev: Event) -> Result<(), Fault>; 695 | } 696 | 697 | pub struct NopSender; 698 | 699 | impl Sender for NopSender { 700 | fn send(&self, _: &str, _: Event) -> Result<(), Fault> { 701 | Ok(()) 702 | } 703 | } 704 | 705 | pub struct UndeliverableSender; 706 | 707 | impl Sender for UndeliverableSender { 708 | fn send(&self, target: &str, _: Event) -> Result<(), Fault> { 709 | Err(Fault::UndeliverableSenderTarget(target.to_owned())) 710 | } 711 | } 712 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate derive_builder; 3 | #[macro_use] 4 | extern crate log; 5 | 6 | extern crate chrono; 7 | extern crate futures; 8 | 9 | pub mod agent; 10 | pub mod ast; 11 | pub mod interpreter; 12 | -------------------------------------------------------------------------------- /tests/assign.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | 3 | extern crate env_logger; 4 | 5 | #[macro_use] 6 | extern crate statechart; 7 | 8 | use statechart::ast::*; 9 | use statechart::interpreter::*; 10 | 11 | #[test] 12 | fn assign_string() { 13 | let _ = env_logger::init(); 14 | let sc = states!{ S { 15 | substates: [ 16 | state!{ S1 { 17 | transitions: [goto!(target: S2)], 18 | on_entry: [action_assign!(key: "hello", value: Value::from_str("assign"))], 19 | }}, 20 | final_state!{ S2 { 21 | result: Output::ValueOf(ValueOfBuilder::default().key("hello").build().unwrap()), 22 | }}, 23 | ]}}; 24 | let ctx = Context::new(sc); 25 | let mut it = Interpreter::new(); 26 | let result = it.run(&ctx); 27 | assert!(result.is_ok(), "fault: {:?}", result.err().unwrap()); 28 | assert_eq!(result.unwrap(), Value::from_str("assign")); 29 | } 30 | -------------------------------------------------------------------------------- /tests/choice.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | #[macro_use] 3 | extern crate statechart; 4 | 5 | use std::rc::Rc; 6 | 7 | use statechart::ast::*; 8 | use statechart::interpreter::*; 9 | 10 | fn a_eq_x(x: i32) -> Condition { 11 | cond_fn!(move |it: &Interpreter| -> bool { 12 | match it.get_var("a") { 13 | Some(&Value::Int(n)) => n == x, 14 | _ => false, 15 | } 16 | }) 17 | } 18 | 19 | #[test] 20 | fn first_choice_only_match() { 21 | let sc = states!{ S { 22 | substates: [ 23 | state!{ S0 { 24 | transitions: [goto!(target: SF)], 25 | on_entry: [action_assign!(key: "a", value: Value::Int(1))], 26 | on_exit: [choose!(when: [ 27 | (a_eq_x(1), 28 | action_assign!(key: "b", value: Value::from_str("matched")))])] 29 | }}, 30 | final_state!{ SF { 31 | result: Output::ValueOf(ValueOfBuilder::default().key("b").build().unwrap()), 32 | }}, 33 | ]}}; 34 | let ctx = Context::new(sc); 35 | let mut it = Interpreter::new(); 36 | let result = it.run(&ctx); 37 | assert!(result.is_ok(), "fault: {:?}", result.err().unwrap()); 38 | assert_eq!(result.unwrap(), Value::from_str("matched")); 39 | } 40 | 41 | #[test] 42 | fn last_match() { 43 | let sc = states!{ S { 44 | substates: [ 45 | state!{ S0 { 46 | transitions: [goto!(target: SF)], 47 | on_entry: [action_assign!(key: "a", value: Value::Int(2))], 48 | on_exit: [choose!(when: [ 49 | (a_eq_x(1), 50 | action_assign!(key: "b", value: Value::from_str("not matched"))), 51 | (a_eq_x(2), 52 | action_assign!(key: "b", value: Value::from_str("matched")))])], 53 | }}, 54 | final_state!{ SF { 55 | result: Output::ValueOf(ValueOfBuilder::default().key("b").build().unwrap()), 56 | }}, 57 | ]}}; 58 | let ctx = Context::new(sc); 59 | let mut it = Interpreter::new(); 60 | let result = it.run(&ctx); 61 | assert!(result.is_ok(), "fault: {:?}", result.err().unwrap()); 62 | assert_eq!(result.unwrap(), Value::from_str("matched")); 63 | } 64 | 65 | #[test] 66 | fn otherwise() { 67 | let sc = states!{ S { 68 | substates: [ 69 | state!{ S0 { 70 | transitions: [goto!(target: SF)], 71 | on_exit: [choose!(when: [ 72 | (a_eq_x(1), 73 | action_assign!(key: "b", value: Value::from_str("not matched"))), 74 | (a_eq_x(2), 75 | action_assign!(key: "b", value: Value::from_str("not matched")))], 76 | otherwise: action_assign!(key: "b", value: Value::from_str("matched")))], 77 | }}, 78 | final_state!{ SF { 79 | result: Output::ValueOf(ValueOfBuilder::default().key("b").build().unwrap()), 80 | }}, 81 | ]}}; 82 | let ctx = Context::new(sc); 83 | let mut it = Interpreter::new(); 84 | let result = it.run(&ctx); 85 | assert!(result.is_ok(), "fault: {:?}", result.err().unwrap()); 86 | assert_eq!(result.unwrap(), Value::from_str("matched")); 87 | } 88 | -------------------------------------------------------------------------------- /tests/pingpong.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | 3 | extern crate env_logger; 4 | 5 | #[macro_use] 6 | extern crate statechart; 7 | 8 | use std::rc::Rc; 9 | 10 | use statechart::ast::*; 11 | use statechart::interpreter::*; 12 | 13 | #[test] 14 | fn pingpong() { 15 | let _ = env_logger::init(); 16 | let sc = states!{ root { 17 | initial_label: Some("init".to_string()), 18 | substates: [ 19 | state!{ init { 20 | on_entry: [ 21 | action_assign!(key: "i", value: Value::Int(0)), 22 | action_raise!(topic: "ping", contents: Value::Int(1))], 23 | transitions: [goto!(target: ping)]}}, 24 | state!{ ping { 25 | transitions: [ 26 | goto!(target: pong, topics: ["ping"], 27 | cond: cond_fn!(|it: &Interpreter| { 28 | match it.get_var("i") { 29 | Some(&Value::Int(i)) => i < 10, 30 | _ => false, 31 | }}), 32 | actions: [ 33 | action_raise!(topic: "pong"), 34 | action_fn!(Rc::new(|it: &mut Interpreter| { 35 | let i = match it.get_var("i") { 36 | Some(&Value::Int(i)) => i, 37 | _ => return Err(Fault::ActionError( 38 | "i: invalid or uninitialized value".to_string())), 39 | }; 40 | it.set_var("i", Value::Int(i+1)); 41 | Ok(()) 42 | }))]), 43 | goto!(target: end, 44 | cond: cond_fn!(|it: &Interpreter| { 45 | match it.get_var("i") { 46 | Some(&Value::Int(i)) => i >= 10, 47 | _ => false, 48 | } 49 | })), 50 | ], 51 | on_entry: [action_log!(message: "ping!")], 52 | }}, 53 | state!{ pong { 54 | transitions: [goto!(target: ping, topics: ["pong"], 55 | actions: [action_raise!(topic: "ping")])], 56 | on_entry: [action_log!(message: "pong!")], 57 | }}, 58 | final_state!{ end { 59 | result: Output::ValueOf(ValueOfBuilder::default().key("i").build().unwrap()), 60 | }}, 61 | ]}}; 62 | let ctx = Context::new(sc); 63 | let mut it = Interpreter::new(); 64 | let result = it.run(&ctx); 65 | assert!(result.is_ok(), "fault: {:?}", result.err().unwrap()); 66 | assert_eq!(result.unwrap(), Value::Int(10)); 67 | } 68 | -------------------------------------------------------------------------------- /tests/stateid.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | 3 | extern crate statechart; 4 | 5 | use statechart::interpreter::Interpreter; 6 | 7 | #[test] 8 | fn common_ancestor() { 9 | let l = vec![0, 1, 1, 3, 4]; 10 | let r = vec![0, 1, 2, 3, 1]; 11 | let a = Interpreter::common_ancestor(&l, &r); 12 | assert_eq!(a, vec![0, 1]); 13 | let a = Interpreter::common_ancestor(&r, &l); 14 | assert_eq!(a, vec![0, 1]); 15 | 16 | let l = vec![0, 1, 1]; 17 | let r = vec![0, 1, 1, 2, 0]; 18 | let a = Interpreter::common_ancestor(&l, &r); 19 | assert_eq!(a, vec![0, 1, 1]); 20 | let a = Interpreter::common_ancestor(&r, &l); 21 | assert_eq!(a, vec![0, 1, 1]); 22 | 23 | let l = vec![]; 24 | let r = vec![0, 1]; 25 | let a = Interpreter::common_ancestor(&l, &r); 26 | assert_eq!(a, vec![]); 27 | let a = Interpreter::common_ancestor(&r, &l); 28 | assert_eq!(a, vec![]); 29 | } 30 | 31 | #[test] 32 | fn exit_states() { 33 | let l = vec![0, 1, 1, 3, 4]; 34 | let r = vec![0, 1, 1]; 35 | let exits = Interpreter::exit_states(&l, &r); 36 | assert_eq!(exits, vec![vec![0, 1, 1, 3, 4], vec![0, 1, 1, 3]]); 37 | let exits = Interpreter::exit_states(&r, &l); 38 | assert!(exits.is_empty()); 39 | } 40 | 41 | #[test] 42 | fn entry_states() { 43 | let l = vec![0, 1, 1]; 44 | let r = vec![0, 1, 1, 3, 4]; 45 | let entries = Interpreter::entry_states(&l, &r); 46 | assert_eq!(entries, vec![vec![0, 1, 1, 3], vec![0, 1, 1, 3, 4]]); 47 | let entries = Interpreter::entry_states(&r, &l); 48 | assert!(entries.is_empty()); 49 | } 50 | -------------------------------------------------------------------------------- /tests/test.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | 3 | extern crate env_logger; 4 | 5 | #[macro_use] 6 | extern crate statechart; 7 | 8 | use std::collections::HashMap; 9 | 10 | use statechart::ast::*; 11 | use statechart::interpreter::*; 12 | 13 | #[test] 14 | fn blocked_indefinitely() { 15 | let st = state!{ S {} }; 16 | let ctx = Context::new(st); 17 | let mut it = Interpreter::new(); 18 | let result = it.run(&ctx); 19 | assert_eq!(result, Err(Fault::BlockedIndefinitely)); 20 | } 21 | 22 | #[test] 23 | fn transitions() { 24 | let ctx = Context::new(c123()); 25 | let st = ctx.root(); 26 | assert_eq!(st.node().id(), &vec![]); 27 | assert_eq!(st.node().substate("S1").unwrap().node().id(), &vec![0]); 28 | assert_eq!(st.node().substate("S2").unwrap().node().id(), &vec![1]); 29 | assert_eq!(st.node().substate("S3").unwrap().node().id(), &vec![2]); 30 | assert_eq!(st.node().substate("nonesuch"), None); 31 | assert_eq!(ctx.state_by_id(&vec![0]).unwrap().node().id(), &vec![0]); 32 | assert_eq!(ctx.state_by_id(&vec![1]).unwrap().node().id(), &vec![1]); 33 | assert_eq!(ctx.state_by_id(&vec![2]).unwrap().node().id(), &vec![2]); 34 | } 35 | 36 | #[test] 37 | fn goto() { 38 | let g = goto!(target: S1, topics: ["foo", "bar", "baz"]); 39 | assert_eq!(g, 40 | TransitionBuilder::default() 41 | .target_label(Some("S1".to_string())) 42 | .topics(["foo", "bar", "baz"].iter().map(|x| x.to_string()).collect()) 43 | .build() 44 | .unwrap()); 45 | } 46 | 47 | #[test] 48 | fn context() { 49 | let ctx = Context::new(c123()); 50 | assert_eq!(ctx.state("S1").unwrap().node().label(), "S1"); 51 | for ss in vec!["S1", "S2", "S3"] { 52 | assert_eq!(ctx.state_by_id(ctx.state(ss).unwrap().node().parent().unwrap()).unwrap().node().label(), 53 | "S"); 54 | } 55 | assert_eq!(ctx.state("S").unwrap().node().parent(), None); 56 | let mut it = Interpreter::new(); 57 | let result = it.run(&ctx); 58 | assert!(result.is_ok(), "{:?}", result.err().unwrap()); 59 | assert_eq!(result.unwrap(), Value::Object(HashMap::new())); 60 | } 61 | 62 | #[test] 63 | fn parallel() { 64 | let ctx = Context::new(phello()); 65 | let mut it = Interpreter::new(); 66 | let result = it.run(&ctx); 67 | assert!(result.is_ok(), "{:?}", result.err().unwrap()); 68 | assert_eq!(result.unwrap(), Value::Object(HashMap::new())); 69 | } 70 | 71 | #[test] 72 | fn parallel_final() { 73 | let ctx = Context::new(phellofinal()); 74 | let mut it = Interpreter::new(); 75 | let result = it.run(&ctx); 76 | assert!(result.is_ok(), "{:?}", result.err().unwrap()); 77 | assert_eq!(result.unwrap(), Value::Object(HashMap::new())); 78 | } 79 | 80 | #[test] 81 | fn parallel_swap() { 82 | let ctx = Context::new(pswap()); 83 | let mut it = Interpreter::new(); 84 | for _ in 0..100 { 85 | let result = it.step(&ctx); 86 | assert_eq!(result, Ok(Status::Runnable)); 87 | } 88 | } 89 | 90 | fn c123() -> State { 91 | let _ = env_logger::init(); 92 | states!{ S { 93 | substates: [ 94 | state!{ S1 { 95 | transitions: [goto!(target: S2)], 96 | on_entry: [action_log!(message: "hello s1")], 97 | }}, 98 | state!{ S2 { 99 | transitions: [goto!(target: S3)], 100 | on_entry: [action_log!(message: "hello s2")], 101 | }}, 102 | final_state!{ S3 { 103 | on_entry: [action_log!(message: "hello s3")], 104 | on_exit: [action_log!(message: "and goodbye now")], 105 | }}, 106 | ]}} 107 | } 108 | 109 | fn phello() -> State { 110 | let _ = env_logger::init(); 111 | states!{ S { 112 | substates: [ 113 | parallel!{ P { 114 | substates: [ 115 | state!{ S1 { 116 | transitions: [goto!(target: SF)], 117 | on_entry: [action_log!(message: "hello s1")], 118 | }}, 119 | state!{ S2 { 120 | on_entry: [action_log!(message: "hello s2")], 121 | }}, 122 | state!{ S3 { 123 | on_entry: [action_log!(message: "hello s3")], 124 | }}, 125 | ]}}, 126 | final_state!{ SF { 127 | on_exit: [action_log!(message: "goodbye now")], 128 | }}, 129 | ]}} 130 | } 131 | 132 | fn phellofinal() -> State { 133 | let _ = env_logger::init(); 134 | states!{ S { 135 | substates: [ 136 | parallel!{ P { 137 | substates: [ 138 | state!{ S1 { 139 | on_entry: [action_log!(message: "hello s1")], 140 | }}, 141 | state!{ S2 { 142 | on_entry: [action_log!(message: "hello s2")], 143 | }}, 144 | state!{ S3 { 145 | on_entry: [action_log!(message: "hello s3")], 146 | }}, 147 | final_state!{ SF { 148 | on_entry: [action_log!(message: "hello sf")], 149 | on_exit: [action_log!(message: "goodbye now")], 150 | }}, 151 | ]}}, 152 | ]}} 153 | } 154 | 155 | fn pswap() -> State { 156 | let _ = env_logger::init(); 157 | states!{ S { 158 | substates: [ 159 | parallel!{ P { 160 | substates: [ 161 | state!{ S1 { 162 | on_entry: [action_log!(message: "s1 wants to be an s2")], 163 | transitions: [goto!(target: S2)], 164 | }}, 165 | state!{ S2 { 166 | on_entry: [action_log!(message: "s2 wants to be an s1")], 167 | transitions: [goto!(target: S1)], 168 | }}, 169 | ]}}, 170 | ]}} 171 | } 172 | --------------------------------------------------------------------------------