diff options
author | Magnus Henoch <mange@freemail.hu> | 2006-07-15 12:15:50 +0000 |
---|---|---|
committer | Kirill A. Korinskiy <catap@catap.ru> | 2006-07-15 12:15:50 +0000 |
commit | 14ae208a951d10661761219d212a3fc04f5c6314 (patch) | |
tree | 91095922c02bcfeb44a693f8127125e5dfb99cef /fsm.el | |
parent | 15b148e570e44b864c176423f54459caabfa4b1d (diff) |
Revision: mange@freemail.hu--2005/emacs-jabber--cvs-head--0--patch-248
Creator: Magnus Henoch <mange@freemail.hu>
Merge file transfer work
Patches applied:
* mange@freemail.hu--2005/emacs-jabber--file-transfer--0--base-0
tag of mange@freemail.hu--2005/emacs-jabber--cvs-head--0--patch-243
* mange@freemail.hu--2005/emacs-jabber--file-transfer--0--patch-1
Merge fsm
* mange@freemail.hu--2005/emacs-jabber--file-transfer--0--patch-2
Commit initial file transfer work
* mange@freemail.hu--2005/emacs-jabber--file-transfer--0--patch-3
Really ignore disco items without nodes
* mange@freemail.hu--2005/emacs-jabber--file-transfer--0--patch-4
Remove debug output
* mange@freemail.hu--2005/emacs-jabber--file-transfer--0--patch-5
Wait for all disco info results, in case we need many proxies
* mange@freemail.hu--2005/emacs-jabber--file-transfer--0--patch-6
Move parentheses
* mange@freemail.hu--2005/emacs-jabber--file-transfer--0--patch-7
Fix termination of file transfers
* mange@freemail.hu--2005/emacs-jabber--file-transfer--0--patch-8
Merge from CVS HEAD
* mange@freemail.hu--2005/emacs-jabber--file-transfer--0--patch-9
Fix stupid bug in jabber-socks5
* mange@freemail.hu--2005/emacs-jabber--file-transfer--0--patch-10
kill-all-local-variables after opening buffer for file being downloaded
* mange@freemail.hu--2005/emacs-jabber--file-transfer--0--patch-11
Load file transfer code by default
* mange@freemail.hu--2005/emacs-jabber--file-transfer--0--patch-12
Remove obsolete documentation. Update NEWS.
* mange@freemail.hu--2005/emacs-jabber--fsm--0--base-0
tag of mange@freemail.hu--2005/emacs-jabber--cvs-head--0--patch-243
* mange@freemail.hu--2005/emacs-jabber--fsm--0--patch-1
Add fsm.el
Diffstat (limited to 'fsm.el')
-rw-r--r-- | fsm.el | 273 |
1 files changed, 273 insertions, 0 deletions
@@ -0,0 +1,273 @@ +;;; fsm.el --- state machine library + +;; Copyright (C) 2006 Magnus Henoch + +;; Author: Magnus Henoch <mange@freemail.hu> +;; Version: 0.1 + +;; This file 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 2, or (at your option) +;; any later version. + +;; This file 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 GNU Emacs; see the file COPYING. If not, write to +;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +;; Boston, MA 02110-1301, USA. + +;;; Commentary: + +;; fsm.el is an exercise in metaprogramming inspired by gen_fsm of +;; Erlang/OTP. It aims to make asynchronous programming in Emacs Lisp +;; easy and fun. By "asynchronous" I mean that long-lasting tasks +;; don't interfer with normal editing. + +;; Some people say that it would be nice if Emacs Lisp had threads +;; and/or continuations. They are probably right, but there are few +;; things that can't be made to run in the background using facilities +;; already available: timers, filters and sentinels. As the code can +;; become a bit messy when using such means, with callbacks everywhere +;; and such things, it can be useful to structure the program as a +;; state machine. + +;; In this model, a state machine passes between different "states", +;; which are actually only different event handler functions. The +;; state machine receives "events" (from timers, filters, user +;; requests, etc) and reacts to them, possibly entering another state, +;; possibly returning a value. + +;; The essential macros/functions are: +;; +;; define-state-machine - create start-FOO function +;; define-state - event handler for each state (required) +;; define-enter-state - called when entering a state (optional) +;; fsm-send - send an event to a state machine +;; fsm-call - send an event and wait for reply + +;; fsm.el is similar to but different from Distel: +;; <URL:http://fresh.homeunix.net/~luke/distel/> +;; Emacs' tq library is a similar idea. + +;;; Code: + +(eval-when-compile (require 'cl)) + +(defvar fsm-debug "*fsm-debug*" + "Name of buffer for fsm debug messages. +If nil, don't output debug messages.") + +(defun fsm-debug-output (format &rest args) + "Append debug output to buffer named by `fsm-debug'. +FORMAT and ARGS are passed to `format'." + (when fsm-debug + (with-current-buffer (get-buffer-create fsm-debug) + (save-excursion + (goto-char (point-max)) + (insert (current-time-string) ": " (apply 'format format args) "\n"))))) + +(defmacro* define-state-machine (name &key start sleep) + "Define a state machine class called NAME. +A function called start-NAME is created, which uses the argument +list and body specified in the :start argument. BODY should +return a list of the form (STATE STATE-DATA [TIMEOUT]), where +STATE is the initial state (defined by `define-state'), +STATE-DATA is any object, and TIMEOUT is the number of seconds +before a :timeout event will be sent to the state machine. BODY +may refer to the instance being created through the dynamically +bound variable `fsm'. + +SLEEP-FUNCTION, if provided, takes one argument, the number of +seconds to sleep while allowing events concerning this state +machine to happen. There is probably no reason to change the +default, which is accept-process-output with rearranged +arguments. + +\(fn NAME :start ((ARG ...) DOCSTRING BODY) [:sleep SLEEP-FUNCTION])" + (let ((start-name (intern (concat "start-" (symbol-name name))))) + (destructuring-bind (arglist docstring &body body) start + (unless (stringp docstring) + (error "Docstring is not a string")) + `(progn + (defun ,start-name ,arglist + ,docstring + (fsm-debug-output "Starting %s" ',name) + (let ((fsm (list :fsm ',name))) + (destructuring-bind (state state-data &optional timeout) + (progn ,@body) + (nconc fsm (list :state nil :state-data nil + :sleep ,(or sleep (lambda (secs) (accept-process-output nil secs))) + :deferred nil)) + (fsm-update fsm state state-data timeout) + fsm))))))) + +(defmacro* define-state (fsm state-name arglist &body body) + "Define a state called STATE-NAME in the state machine FSM. +ARGLIST and BODY make a function that gets called when the state +machine receives an event in this state. The arguments are: + +FSM the state machine instance (treat it as opaque) +STATE-DATA An object +EVENT The occurred event, an object. +CALLBACK A function of one argument that expects the response + to this event, if any (often `ignore' is used) + +If the event should return a response, the state machine should +arrange to call CALLBACK at some point in the future (not necessarily +in this handler). + +The function should return a list of the form (NEW-STATE +NEW-STATE-DATA TIMEOUT): + +NEW-STATE The next state, a symbol +NEW-STATE-DATA An object +TIMEOUT A number: send timeout event after this many seconds + nil: cancel existing timer + :keep: let existing timer continue + +Alternatively, the function may return the keyword :defer, in +which case the event will be resent when the state machine enters +another state." + (let ((fn-name (intern (concat "fsm-" (symbol-name fsm) "-" (symbol-name state-name))))) + `(defun ,fn-name ,arglist + ,@body))) + +(defmacro* define-enter-state (fsm state-name arglist &body body) + "Define a function to call when FSM enters the state STATE-NAME. +ARGLIST and BODY make a function that gets called when the state +machine enters this state. The arguments are: + +FSM the state machine instance (treat it as opaque) +STATE-DATA An object + +The function should return a list of the form (NEW-STATE-DATA +TIMEOUT): + +NEW-STATE-DATA An object +TIMEOUT A number: send timeout event after this many seconds + nil: cancel existing timer + :keep: let existing timer continue" + (let ((fn-name (intern (concat "fsm-" (symbol-name fsm) "-enter-" (symbol-name state-name))))) + `(defun ,fn-name ,arglist + ,@body))) + +(defun fsm-start-timer (fsm secs) + "Send a timeout event to FSM after SECS seconds. +The timer is canceled if another event occurs before, unless the +event handler explicitly asks to keep the timer." + (fsm-stop-timer fsm) + (setf (cddr fsm) + (plist-put + (cddr fsm) + :timeout (run-with-timer secs + nil + #'fsm-send-sync fsm + :timeout)))) + +(defun fsm-stop-timer (fsm) + "Stop the timeout timer of FSM." + (let ((timer (plist-get (cddr fsm) :timeout))) + (when (timerp timer) + (cancel-timer timer) + (setf (cddr fsm) (plist-put (cddr fsm) :timeout nil))))) + +(defun fsm-maybe-change-timer (fsm timeout) + "Change the timer of FSM according to TIMEOUT." + (cond + ((numberp timeout) + (fsm-start-timer fsm timeout)) + ((null timeout) + (fsm-stop-timer fsm)) + ;; :keep needs no timer change + )) + +(defun fsm-send (fsm event &optional callback) + "Send EVENT to FSM asynchronously. +If the state machine generates a response, eventually call +CALLBACK with the response as only argument." + (run-with-timer 0.1 nil #'fsm-send-sync fsm event callback)) + +(defun fsm-update (fsm new-state new-state-data timeout) + (let ((old-state (plist-get (cddr fsm) :state))) + (plist-put (cddr fsm) :state new-state) + (plist-put (cddr fsm) :state-data new-state-data) + (fsm-maybe-change-timer fsm timeout) + + ;; On state change, call enter function and send deferred events + ;; again. + (unless (eq old-state new-state) + (fsm-debug-output "%s enters %s" (cadr fsm) new-state) + (let ((enter-fn (intern (concat "fsm-" (symbol-name (cadr fsm)) "-enter-" (symbol-name new-state))))) + (when (functionp enter-fn) + (destructuring-bind (newer-state-data newer-timeout) + (funcall enter-fn fsm new-state-data) + (plist-put (cddr fsm) :state-data newer-state-data) + (fsm-maybe-change-timer fsm newer-timeout)))) + + (let ((deferred (nreverse (plist-get (cddr fsm) :deferred)))) + (setf (cddr fsm) + (plist-put (cddr fsm) :deferred nil)) + (dolist (event deferred) + (apply 'fsm-send-sync fsm event)))))) + +(defun fsm-send-sync (fsm event &optional callback) + "Send EVENT to FSM synchronously. +If the state machine generates a response, eventually call +CALLBACK with the response as only argument." + (save-match-data + (let* ((fsm-name (second fsm)) + (state (plist-get (cddr fsm) :state)) + (state-data (plist-get (cddr fsm) :state-data)) + (state-fn (intern (concat "fsm-" (symbol-name fsm-name) + "-" (symbol-name state))))) + (fsm-debug-output "Sent %S to %s in state %s" event fsm-name state) + (let ((result (condition-case e + (funcall state-fn fsm state-data event (or callback 'ignore)) + (error (cons :error-signaled e))))) + ;; Special case for deferring an event until next state change. + (cond + ((eq result :defer) + (let ((deferred (plist-get (cddr fsm) :deferred))) + (plist-put (cddr fsm) :deferred (cons (list event callback) deferred)))) + ((null result) + (fsm-debug-output "Warning: event %S ignored in state %s" event state)) + ((eq (car-safe result) :error-signaled) + (fsm-debug-output "Error: %s" (error-message-string (cdr result)))) + (t + (destructuring-bind (new-state new-state-data &optional timeout) result + (fsm-update fsm new-state new-state-data timeout)))))))) + +(defun fsm-call (fsm event) + "Send EVENT to FSM synchronously, and wait for a reply. +Return the reply. +`with-timeout' might be useful." + (lexical-let (reply) + (fsm-send-sync fsm event (lambda (r) (setq reply (list r)))) + (while (null reply) + (fsm-sleep fsm 1)) + (car reply))) + +(defun fsm-make-filter (fsm) + "Return a filter function that sends events to FSM. +Events sent are of the form (:filter PROCESS STRING)." + (lexical-let ((fsm fsm)) + (lambda (process string) + (fsm-send-sync fsm (list :filter process string))))) + +(defun fsm-make-sentinel (fsm) + "Return a sentinel function that sends events to FSM. +Events sent are of the form (:sentinel PROCESS STRING)." + (lexical-let ((fsm fsm)) + (lambda (process string) + (fsm-send-sync fsm (list :sentinel process string))))) + +(defun fsm-sleep (fsm secs) + "Sleep up to SECS seconds in a way that lets FSM receive events." + (funcall (plist-get (cddr fsm) :sleep) secs)) + +(provide 'fsm) +;;; fsm.el ends here |