machine.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357
  1. /*
  2. Package fsm defines an embeddable finite state machine.
  3. In its initial version, it is a straight port of the 2012 PHP
  4. OSInet\Finite_State_Machine\Machine available in https://code.osinet.fr/fgm/php__phplib.git
  5. See http://wiki.audean.com/fsm/fsm for details.
  6. Copyright (c) 2023 Ouest Systèmes Informatiques
  7. License GPL-3.0
  8. */
  9. package fsm
  10. import (
  11. "errors"
  12. "fmt"
  13. "log"
  14. "golang.org/x/exp/maps"
  15. "golang.org/x/exp/slices"
  16. )
  17. type (
  18. Event any
  19. EventMode int
  20. State string
  21. Transition map[Event][2]Result
  22. )
  23. const (
  24. Version string = "12D22"
  25. IdleEvent = "idle" // must not change name: method fIdle depends on it
  26. InitState State = "init"
  27. FinalState State = "final"
  28. )
  29. const (
  30. EventInvalid EventMode = iota // Should not be seen
  31. EventNormal // Processes events
  32. EventQueue // Queue events for later use
  33. EventSink // Throw away events
  34. )
  35. // Machine is a composable state machine.
  36. //
  37. // Applications must create an actual FSM composing such a Machine, in which they will:
  38. // - define the fTransitions table, usually in their constructor
  39. // - invoke the parent constructor to set up the FSM from the transitions table
  40. // - create "f_foo" functions for each "foo" event, except "idle", which is builtin.
  41. //
  42. // Applications may:
  43. // - disable event processing by setting $this->event to one of the fsm::EVENT_* constants
  44. // - disable post-event actions by setting $this->allowsActions to false
  45. // - disable the builtin idle event by setting $this->idleWork to false
  46. // - query the current state by using $this->getState()
  47. // - send an idle event by using $this->idle()
  48. // - submit any event (including idle) by using $this->ApplyEvent($eventName)
  49. type Machine struct {
  50. AllowActions bool
  51. EventMode EventMode
  52. IdleWork bool
  53. Queue []any
  54. State State
  55. Transitions map[State]Transition
  56. }
  57. // checkTransitions verifies than a transitions graph has been defined.
  58. //
  59. // Since applications are not expected to modify the machine once initialized,
  60. // it is not meant to be used beyond New.
  61. func (m Machine) checkTransitions() error {
  62. if len(m.Transitions) == 0 {
  63. return errors.New("no FSM processing is allowed without a transitions table")
  64. }
  65. if _, ok := m.Transitions[InitState]; ok {
  66. return errors.New("transitions map does not have any initial state")
  67. }
  68. return nil
  69. }
  70. // New initializes the FSM to the first state in the transitions table
  71. func New(transitions map[State]Transition) (*Machine, error) {
  72. m := Machine{
  73. AllowActions: true,
  74. IdleWork: true,
  75. EventMode: EventNormal,
  76. Queue: []any{}, // Queue is the event queue for EVENT_QUEUE mode
  77. State: InitState, // State holds the current state of the machine
  78. Transitions: transitions, // Transitions holds the transitions table
  79. }
  80. if err := m.checkTransitions(); err != nil {
  81. return nil, err
  82. }
  83. return &m, nil
  84. }
  85. // AcceptedEvents returns the list of events accepted in the current state.
  86. func (m Machine) AcceptedEvents() []Event {
  87. t, ok := m.Transitions[m.State]
  88. if !ok {
  89. return nil
  90. }
  91. events := maps.Keys(t)
  92. return events
  93. }
  94. // AcceptedOutcomes returns the list of outcomes accepted in the current state for the given event
  95. func (m Machine) AcceptedOutcomes(e Event) ([2]Result, error) {
  96. res, ok := m.Transitions[m.State][e]
  97. if !ok {
  98. return [2]Result{}, fmt.Errorf("event %q not accepted in state %q", e, m.State)
  99. }
  100. return res, nil
  101. }
  102. // IsEventAllowed verifies whether the given event is accepted in the current state
  103. func (m Machine) IsEventAllowed(e Event) bool {
  104. t := m.Transitions[m.State]
  105. isIt := slices.Contains(maps.Keys(t), e)
  106. return isIt
  107. }
  108. // IsOutcomeAllowed checks whether a given outcome available for a given event, considering the current state.
  109. func (m Machine) IsOutcomeAllowed(e Event, outcome Result) bool {
  110. if !m.IsEventAllowed(e) {
  111. return false
  112. }
  113. res, err := m.AcceptedOutcomes(e)
  114. if err != nil {
  115. return false
  116. }
  117. isIt := outcome == res[0] || outcome == res[1]
  118. return isIt
  119. }
  120. // ApplyEvent applies both the given event and the resulting event chain if applicable.
  121. func (m *Machine) ApplyEvent(e Event) Result {
  122. var res Result
  123. for {
  124. var next Event
  125. res = m.ApplySimpleEvent(e)
  126. if m.AllowActions {
  127. next = res.Action
  128. } else {
  129. next = nil
  130. }
  131. if next != nil {
  132. break
  133. }
  134. }
  135. return res
  136. }
  137. // ApplySimpleEvent is a helper for ApplyEvent that does not implement the post-transition action
  138. //
  139. // See ApplyEvent
  140. func (m *Machine) ApplySimpleEvent(e Event) Result {
  141. currentState := m.State
  142. if e == IdleEvent && !m.IdleWork {
  143. return Result{}
  144. }
  145. if !m.IsEventAllowed(e) {
  146. log.Fatalf("Event %q not allowed in current state %q.\n", e, currentState)
  147. }
  148. return Result{}
  149. }
  150. $outcomes = $this- > getAcceptedOutcomes($eventName)
  151. $methodName = "event$eventName"
  152. if !method_exists($this, $methodName)) {
  153. die (func_name().": missing method "
  154. .get_class($this)."::$methodName. Aborting.\n")
  155. }
  156. $outcome = $this- >$methodName()
  157. if !in_array($outcome, $outcomes)) {
  158. throw new \Exception(func_name()
  159. .": event guard. Transition on \"$eventName\" return invalid outcome: "
  160. .print_r($outcome, true)
  161. ." for state $this->fState\n")
  162. }
  163. $transition = &$this- > fTransitions[$currentState][$eventName][$outcome]
  164. $result = new Result (
  165. $outcome,
  166. $transition[0],
  167. $transition[1]
  168. )
  169. if (isset($result->fsmState)) {
  170. $this->fState = $result->fsmState
  171. }
  172. /* print_r($this->fTransitions[$currentState][$eventName]);
  173. var_dump($result); */
  174. if (!isset($outcome)) {
  175. $outcome = 'NULL'
  176. }
  177. /*
  178. echo func_name()
  179. . ": $currentState: " . $eventName . '[' . $outcome . ']'
  180. . " -> $this->fState / " . $result->fsmAction . PHP_EOL;
  181. */
  182. return result
  183. }
  184. /**
  185. * Default event.
  186. *
  187. * @return boolean
  188. */
  189. public function fIdle() {
  190. return TRUE
  191. }
  192. /**
  193. * Apply an fsm::IDLE_EVENT event. Do not confuse with fIdle !
  194. *
  195. * @return Result
  196. */
  197. public function idle() {
  198. return $this->applyEvent(self::IDLE_EVENT)
  199. }
  200. /**
  201. * return the current operating mode of the FSM
  202. * @return int
  203. */
  204. public function getEventMode() {
  205. return $this->fEventMode
  206. }
  207. /**
  208. * set the current operating mode for the FSM
  209. *
  210. * @param int $mode fsm::EVENT_* constants
  211. * @return int $mode fsm::EVENT_* constants
  212. */
  213. public function setEventMode($mode) {
  214. switch ($mode) {
  215. case self::EVENT_NORMAL:
  216. if (count($this->fQueue) > 0) {
  217. while (($event = array_shift($this->fQueue)) != = NULL) {
  218. $this->applyEvent($event)
  219. }
  220. }
  221. break
  222. // Nothing special to do
  223. case self::EVENT_QUEUE:
  224. break
  225. // Empty queue if needed
  226. case self::EVENT_SINK:
  227. if (count($this->fQueue) > 0) {
  228. $this->fQueue = array()
  229. }
  230. break
  231. default:
  232. throw new Exception("Trying to set unknown FSM mode $mode")
  233. }
  234. $this->fEventMode = $mode
  235. return $this->fEventMode
  236. }
  237. /**
  238. * Load the fTransitions table from an external resource.
  239. *
  240. * @param string $url
  241. * Optional: defaults to the name of the class, . ".xml"
  242. */
  243. public function loadFsm($url = NULL) {
  244. $osd = FALSE // on screen display (debug)
  245. if ($osd) {
  246. echo "Loading FSM from $url\n"
  247. }
  248. if (!isset($url)) {
  249. $url = get_class($this).".xml"
  250. }
  251. $fsm = simplexml_load_file($url)
  252. $fsmVersion = (string) $fsm['fsm_version']
  253. if ($fsmVersion != = '1.3') {
  254. die("Revision $fsmVersion of schema is not supported.\n")
  255. }
  256. $this->idleWork = ($fsm['idle_work'] == 1)
  257. $this->allowActions = ($fsm['allow_actions'] == 1)
  258. $this->fTransitions = array()
  259. $t = &$this->fTransitions
  260. // (string) casts in this loop are required: RHS is a SimpleXMLElement
  261. foreach ($fsm->state as $state) {
  262. $id = (string) $state['id']
  263. if ($osd) {
  264. echo "State $id :\n"
  265. }
  266. $t[$id] = array()
  267. switch ($id) {
  268. case self::INIT_STATE:
  269. if ($osd) {
  270. echo " Initial state\n"
  271. }
  272. break
  273. case self::FINAL_STATE:
  274. if ($osd) {
  275. echo " Final state\n"
  276. }
  277. break
  278. }
  279. foreach ($state->event as $event) {
  280. $name = (string) $event['name']
  281. if ($osd) {
  282. echo " Event $name"
  283. }
  284. if (!isset($event['type'])) {
  285. $event['type'] = 'void'
  286. }
  287. $eventType = (string) $event['type']
  288. if ($osd) {
  289. echo ", type $eventType\n"
  290. }
  291. foreach ($event as $next) {
  292. if ($event['type'] == 'void') {
  293. $next['result'] = 'always'
  294. $result = null
  295. } else {
  296. $result = (string) $next['result']
  297. }
  298. if (!isset($next['state'])) {
  299. $next['state'] = (string) $state['id']
  300. }
  301. if ($osd) {
  302. echo " Next(".$next['result'].') = '.$next['state']
  303. if (isset($next['action'])) {
  304. echo " + event ".$next['action']
  305. }
  306. echo PHP_EOL
  307. }
  308. $t[$id][$name][$result] = array(
  309. (string) $next['state'],
  310. (string) $next['action'])
  311. }
  312. }
  313. }
  314. }
  315. }