machine.go 8.2 KB

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