Files
emacs-dotfiles/load/simple-state-machines.el

177 lines
8.0 KiB
EmacsLisp

;;; simple-state-machines.el --- My simple state machine macro. -*- lexical-binding: t -*-
;; Author: iamtoaster
;; Version: 0.1
;; Package-Requires: (cl-lib)
;; Homepage:
;; Keywords: FSM, Macro
;; This file is not part of GNU Emacs
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; This is my implementation of simple finite state machines. This package
;; provides a defstate-machine macro and an update-state function. They can be
;; combined to create a simple FSM with three states like so:
;;
;; (ssm-defstate-machine test
;; :states (one two three)
;; :initial-state three
;; :transitions ((one . two)
;; (two . three)
;; (three . one))
;; :handlers ((two . (lambda (name) (ssm-update-state name 'three)))
;; (three . (lambda (name) (ssm-update-state name 'one)))
;; (one . (lambda (name) (ssm-update-state name 'two)))))
;;
;; The code above will create a state machine with three states, appropriately
;; named 'one', 'two' and 'three', by specifying them after the :states key. The
;; initial state will be set to state 'three'. If no initial state is provided,
;; the first state in the list will be used automatically.
;;
;; Then, we define valid transitions between states using the :transitions key.
;; If we try to change the state in a way that is not defined in this alist,
;; `update-state' will signal an error.
;;
;; Finally, we specify handlers for states using the :handlers key. See
;; `defstate-machine' for information.
;;
;; This macro call will provide us with the following variables:
;; 'test-state-table' -- Stores the state list, verbatim.
;; 'test-transition-table' -- Stores the valid transitions, verbatim.
;; 'test-current-state' -- Stores the current state.
;; 'test-state-data' -- Optional data for the current state, initialized to nil.
;; 'test-state-handlers' -- Stores the mapping from the states to their handlers,
;; verbatim.
;;
;; As well as the following functions:
;; 'test-step' -- Calls the handler for the current state.
;; 'test-set-state' -- Tries to set the current state to the provided one, and
;; signals an error if the transition is invalid.
;;; Code:
(require 'cl-lib)
(defmacro ssm-defstate-machine (name &rest args)
"Defines a state machine with name NAME, STATES and TRANSITIONS between
them. If INITIAL-STATE is nil, the first state will be used.
Arguments are specified as keyword/argument pairs. The following
arguments are defined:
:states LIST -- LIST should be a list of symbols each representing a
state. LIST will not be evaluated.
:transitions ALIST -- ALIST should be an alist of the form (FROM . TO)
where both FROM and TO are names of states available in the :states LIST.
:handlers ALIST -- ALIST should be an alist of the form (STATE .
HANDLER) where STATE is one of the states in the :states LIST and
HANDLER is a function that can be called by funcall. It is advised to
provide a handler for all of the state, because an error will be
signalled if you try to step on a state with no handler.
The following optional arguments are defined:
:initial-state SYMBOL -- If SYMBOL is non-nil and present in the :states
LIST, it will be set as the initial state of this state machine.
Otherwise, the first state in the :states LIST will be used.
(fn NAME &key INITIAL-STATE STATES TRANSITIONS HANDLERS)"
(declare (indent defun))
(let ((machine-name (symbol-name name))
(states (plist-get args :states))
(transitions (plist-get args :transitions))
(handlers (plist-get args :handlers))
(initial-state (plist-get args :initial-state)))
(if (not initial-state)
(setq initial-state (car states)))
(cl-assert (cl-find initial-state states) t
"Cannot find the initial state in the state list.")
(dolist (entry handlers)
(cl-assert (cl-find (car entry) states)) t)
(dolist (entry transitions)
(cl-assert (and (cl-find (car entry) states)
(cl-find (cdr entry) states))
nil "Invalid transition: Either the source or the target state does not exist."))
(let ((state-table (intern (concat machine-name "-state-table")))
(transition-table (intern (concat machine-name "-transition-table")))
(current-state (intern (concat machine-name "-current-state")))
(state-data (intern (concat machine-name "-state-data")))
(step-fn (intern (concat machine-name "-step")))
(state-set-fn (intern (concat machine-name "-set-state")))
(handlers-table (intern (concat machine-name "-state-handlers"))))
`(progn
(defvar ,state-table ',states
,(concat "The state table of the " machine-name " state machine."))
(defvar ,transition-table ',transitions
,(concat "The transitions table of the " machine-name " state machine.\n
Each entry of this alist has the form (FROM . TO)."))
(defvar ,current-state ',initial-state
,(concat "The current state of the " machine-name " state machine."))
(defvar ,state-data nil
,(concat "The data of the current state of the " machine-name " state machine."))
(defvar ,handlers-table ',handlers
,(concat "An alist mapping states of the "
machine-name
" state machine to their respective handlers."))
,@(let ((handler (make-symbol "handler"))
(prev-state (make-symbol "prev-state")))
`((defun ,step-fn ()
,(concat "The stepping function of the "
machine-name
" state machine.\n\nIt tries to find a handler for the current state (see `"
(symbol-name current-state)
"' and\n`"
(symbol-name handlers-table)
"'),and then call it with the name of this state machine\nas its only argument.")
(let ((,handler (alist-get ,current-state ,handlers-table))
(,prev-state ,current-state))
(cl-assert ,handler nil "There is no handler for the current state")
(funcall ,handler ,machine-name)
(cl-assert
(cl-find
(cons ,prev-state ,current-state)
,transition-table
:test 'equal)
"Invalid state transition. State machine is now in inconsistent state.")))
(defun ,state-set-fn (state &optional new-data)
,(concat
"Sets the state of "
machine-name
" state machine. Signals an error if the state transition\nis invalid, see `"
(symbol-name transition-table)
"' for a list of valid transitions.")
(cl-assert
(cl-find
(cons ,current-state state)
,transition-table
:test 'equal) t
"Invalid state transition.")
(setq ,current-state state
,state-data new-data))))))))
(defun ssm-update-state (machine new-state &optional new-data)
"Updates the state of the state machine MACHINE. Signals an error if the
state transition is invalid."
(let ((state-fn
(intern-soft (concat machine "-set-state"))))
(funcall state-fn new-state new-data)))
(provide 'simple-state-machines)
;;; simple-state-machines.el ends here