;;; rudel-state-machine.el --- A simple state machine for Rudel ;; ;; Copyright (C) 2009 Jan Moringen ;; ;; Author: Jan Moringen ;; Keywords: Rudel, FSM ;; X-RCS: $Id:$ ;; ;; This file is part of Rudel. ;; ;; Rudel 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. ;; ;; Rudel 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 Rudel. If not, see . ;;; Commentary: ;; ;; This is a simple implementation of a finite state machine ;; (FSM). The is modeled by rudel-state-machine class, objects of ;; which contain state objects of classes derived from ;; rudel-state. There are no explicit transition rules, since states ;; specify their successors. ;;; History: ;; ;; 0.1 - Initial revision ;;; Code: ;; (eval-when-compile (require 'cl)) (require 'eieio) (require 'rudel-errors) (require 'rudel-compat) ;; for pulsing progress reporter ;;; Errors related to the state machine ;; ;; rudel-state-error (intern "rudel-state-error") (put 'rudel-state-error 'error-conditions '(error rudel-error rudel-state-error)) (put 'rudel-state-error 'error-message "Invalid state or state transition") ;; rudel-invalid-successor-state (intern "rudel-invalid-successor-state") (put 'rudel-invalid-successor-state 'error-conditions '(error rudel-error rudel-state-error rudel-invalid-successor-state)) (put 'rudel-invalid-successor-state 'error-message "Invalid successor state in state transition") ;; rudel-entered-error-state (intern "rudel-entered-error-state") (put 'rudel-entered-error-state 'error-conditions '(error rudel-error rudel-state-error rudel-entered-error-state)) (put 'rudel-entered-error-state 'error-message "Transition to error state") ;; rudel-no-start-state (intern "rudel-no-start-state") (put 'rudel-no-start-state 'error-conditions '(error rudel-error rudel-state-error rudel-no-start-state)) (put 'rudel-no-start-state 'error-message "No start state specified for state machine") ;;; Class rudel-state ;; (defclass rudel-state () () "A state that can be used in state machines." :abstract t) (defgeneric rudel-accept ((this rudel-state) &rest arguments) "Executed when the machine receives an event while in state THIS.") (defgeneric rudel-enter ((this rudel-state) &rest arguments) "Executed when the machine switches to state THIS.") (defgeneric rudel-leave ((this rudel-state)) "Executed when the machine leaves state THIS.") ;;; Class rudel-state-machine ;; (defclass rudel-state-machine () ((states :initarg :states :type list ;; alist :initform nil :documentation "A list (NAME . STATE) conses where NAME is a symbol and STATE is an object of a class derived from rudel-state.") (state :initarg :state :type rudel-state-child :documentation "The current state of the machine.")) "A finite state machine.") (defmethod initialize-instance :after ((this rudel-state-machine) &rest slots) "Set current state of THIS to a proper initial value. If a start state is specified in the arguments to the constructor, that state is used. If there is no such state, the list of states is search for a state named start. If that fails as well, the first state in the state list is used." (with-slots (states) this ;; Find a suitable start state and switch to it. (let ((start (or (plist-get slots :start) (car (assoc 'start states)) (when (length states) (car (nth 0 states)))))) (unless start (signal 'rudel-no-start-state nil)) ;; Make start state the current state and call send an enter ;; message. (let ((start (cdr (assoc start states)))) (oset this :state start) (rudel--switch-to-return-value this start (rudel-enter start))))) ) (defmethod rudel-find-state ((this rudel-state-machine) name) "Return state object for symbol NAME." (with-slots (states) this (cdr (assoc name states)))) (defmethod rudel-register-state ((this rudel-state-machine) name state) "Register STATE and its NAME with THIS state machine." (object-add-to-list this :states (cons name state) t)) (defmethod rudel-register-states ((this rudel-state-machine) states) "Register STATES with THIS state machine. STATES is a list of cons cells whose car is a symbol - the name of the state - and whose cdr is a class." (dolist (symbol-and-state states) (destructuring-bind (name . class) symbol-and-state (rudel-register-state this name (make-instance class (symbol-name name))))) ) (defmethod rudel-current-state ((this rudel-state-machine) &optional object) "Return name and, optionally, state object of the current state of THIS. If OBJECT is non-nil, (NAME . OBJECT) is returned. Otherwise, just NAME." (with-slots (states state) this (let ((state-symbol (car (find state states :key #'cdr :test #'eq)))) (if object (cons state-symbol state) state-symbol))) ) (defmethod rudel-accept ((this rudel-state-machine) &rest arguments) "Process an event described by ARGUMENTS." (with-slots (state) this ;; Let the current state decide which state is next. (let ((next (apply #'rudel-accept state arguments))) (cond ;; If NEXT is nil, a symbol or a state object, we switch states ;; without passing any data. ((or (null next) (symbolp next) (rudel-state-child-p next)) (rudel-switch this next)) ;; If NEXT is a list, it contains the symbol of the successor ;; state and additional data. ((listp next) (apply #'rudel-switch this next)) ;; Other types cannot be processed. (t (signal 'wrong-type-argument (list (type-of next))))))) ) (defmethod rudel-switch ((this rudel-state-machine) next &rest arguments) "Leave current state and switch to state NEXT. ARGUMENTS are passed to the `rudel-enter' method of the successor state." (with-slots (states state) this (cond ;; When NEXT is a state object, use it. ((rudel-state-child-p next)) ;; When NEXT is nil, stay in the current state. ((null next) (setq next state)) ;; When NEXT is a symbol (but not nil), look up the corresponding ;; state. Signal an error, if there is none. ((symbolp next) (let ((next-state (assoc next states))) (unless next-state (signal 'rudel-invalid-successor-state (list next '<- state))) (setq next (cdr next-state)))) ;; Other types cannot be processed. (t (signal 'wrong-type-argument (list (type-of next))))) ;; Unless the successor state is equal to the current state, leave ;; the current state and switch to the successor. (if (and (eq state next) (null arguments)) ;; Return state state ;; Notify (old) current state. (rudel-leave state) ;; Update current state. (setq state next) ;; Notify (new) current state. Using the call's value as next ;; state is a bit dangerous since a long sequence of immediate ;; state switches could exhaust the stack. (rudel--switch-to-return-value this state (apply #'rudel-enter state arguments)))) ) (defmethod rudel--switch-to-return-value ((this rudel-state-machine) state next) "Switch from STATE to the next state indicated by NEXT. STATE is the current state. NEXT can nil, a list or a `rudel-state' object." (cond ;; Remain in current state. ((null next) state) ;; NEXT contains next state and arguments to pass to it when ;; switching. ((listp next) (apply #'rudel-switch this next)) ;; Otherwise NEXT is a `rudel-state' object. (t (rudel-switch this next))) ) (defmethod object-print ((this rudel-state-machine) &rest strings) "Add current state to the string representation of THIS." (if (slot-boundp this 'state) (with-slots (state) this (apply #'call-next-method this (format " state: %s" (object-name-string state)) strings)) (call-next-method this " state: #start")) ) ;;; Miscellaneous functions ;; (defun rudel-state-wait (machine success-states &optional error-states callback) "Repeatedly call CALLBACK until MACHINE is in a state in SUCCESS-STATES or ERROR-STATES. MACHINE should be of type rudel-state-machine-child or at least have a method `rudel-get-state'. SUCCESS-STATES and ERROR-STATES are lists which contain the names (as symbols) of success and error states respectively. This function does not return when MACHINE enters states not in SUCCESS-STATES or ERROR-STATES. As a result, a deadlock can occur when MACHINE deadlocks or cycles through states not in either list infinitely. When non-nil, CALLBACK has to be a function that accepts one argument of the form (SYMBOL . STATE) where SYMBOL is the name symbol of the current state and STATE is the state object." ;; Wait until MACHINE enter a state in SUCCESS-STATES or ;; ERROR-STATES. (let ((result (catch 'state-wait (while t ;; Retrieve current state. (destructuring-bind (symbol . state) (rudel-current-state machine t) ;; Check against success and error states. (when (memq symbol success-states) (throw 'state-wait (cons 'success (cons symbol state)))) (when (memq symbol error-states) (throw 'state-wait (cons 'error (cons symbol state)))) ;; Update progress indicator and sleep. (when callback (funcall callback (cons symbol state))) (sleep-for 0.05)))))) (when callback (funcall callback t)) ;; If MACHINE ended up in an error state, signal (unless (eq (car result) 'success) (signal 'rudel-entered-error-state (cdr result))) ;; Return state (cdr result)) ) (provide 'rudel-state-machine) ;;; rudel-state-machine.el ends here