]> code.delx.au - gnu-emacs-elpa/blob - packages/fsm/fsm.el
fsm: Delete trailing whitespace
[gnu-emacs-elpa] / packages / fsm / fsm.el
1 ;;; fsm.el --- state machine library
2
3 ;; Copyright (C) 2006, 2007, 2008 Magnus Henoch
4
5 ;; Author: Magnus Henoch <mange@freemail.hu>
6 ;; Version: 0.1ttn4
7
8 ;; This file is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation; either version 2, or (at your option)
11 ;; any later version.
12
13 ;; This file is distributed in the hope that it will be useful,
14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ;; GNU General Public License for more details.
17
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with GNU Emacs; see the file COPYING. If not, write to
20 ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
21 ;; Boston, MA 02110-1301, USA.
22
23 ;;; Commentary:
24
25 ;; fsm.el is an exercise in metaprogramming inspired by gen_fsm of
26 ;; Erlang/OTP. It aims to make asynchronous programming in Emacs Lisp
27 ;; easy and fun. By "asynchronous" I mean that long-lasting tasks
28 ;; don't interfer with normal editing.
29
30 ;; Some people say that it would be nice if Emacs Lisp had threads
31 ;; and/or continuations. They are probably right, but there are few
32 ;; things that can't be made to run in the background using facilities
33 ;; already available: timers, filters and sentinels. As the code can
34 ;; become a bit messy when using such means, with callbacks everywhere
35 ;; and such things, it can be useful to structure the program as a
36 ;; state machine.
37
38 ;; In this model, a state machine passes between different "states",
39 ;; which are actually only different event handler functions. The
40 ;; state machine receives "events" (from timers, filters, user
41 ;; requests, etc) and reacts to them, possibly entering another state,
42 ;; possibly returning a value.
43
44 ;; The essential macros/functions are:
45 ;;
46 ;; define-state-machine - create start-FOO function
47 ;; define-state - event handler for each state (required)
48 ;; define-enter-state - called when entering a state (optional)
49 ;; define-fsm - encapsulates the above three (more sugar!)
50 ;; fsm-send - send an event to a state machine
51 ;; fsm-call - send an event and wait for reply
52
53 ;; fsm.el is similar to but different from Distel:
54 ;; <URL:http://fresh.homeunix.net/~luke/distel/>
55 ;; Emacs' tq library is a similar idea.
56
57 ;; Here is a simple (not using all the features of fsm.el) example:
58 ;;
59 ;; (require 'cl)
60 ;; (labels ((hey (n ev)
61 ;; (message "%d (%s)\tp%sn%s!" n ev
62 ;; (if (zerop (% n 4)) "o" "i")
63 ;; (make-string (max 1 (abs n)) ?g))))
64 ;; (macrolet ((zow (next timeout)
65 ;; `(progn (hey (incf count) event)
66 ;; (list ,next count ,timeout))))
67 ;; (define-fsm pingpong
68 ;; :start ((init) "Start a pingpong fsm."
69 ;; (interactive "nInit (number, negative to auto-terminate): ")
70 ;; (list :ping (ash (ash init -2) 2) ; 4 is death
71 ;; (when (interactive-p) 0)))
72 ;; :state-data-name count
73 ;; :states
74 ;; ((:ping
75 ;; (:event (zow :pingg 0.1)))
76 ;; (:pingg
77 ;; (:event (zow :pinggg 0.1)))
78 ;; (:pinggg
79 ;; (:event (zow :pong 1)))
80 ;; (:pong
81 ;; (:event (zow :ping (if (= 0 count)
82 ;; (fsm-goodbye-cruel-world 'pingpong)
83 ;; 3))))))))
84 ;; (fsm-send (start-pingpong -16) t)
85 ;;
86 ;; Copy into a buffer, uncomment, and type M-x eval-buffer RET.
87 ;; Alternatively, you can replace the `fsm-goodbye-cruel-world'
88 ;; form with `nil', eval just the `labels' form and then type
89 ;; M-x start-pingpong RET -16 RET.
90
91 ;; Version 0.2:
92 ;; -- Delete trailing whitespace.
93
94 ;; NOTE: This is version 0.1ttn4 of fsm.el, with the following
95 ;; mods (an exercise in meta-meta-programming ;-) by ttn:
96 ;; -- Refill for easy (traditional 80-column) perusal.
97 ;; -- New var `fsm-debug-timestamp-format'.
98 ;; -- Make variables satisfy `user-variable-p'.
99 ;; -- Use `format' instead of `concat'.
100 ;; -- New func `fsm-goodbye-cruel-world'.
101 ;; -- Make start-function respect `interactive' spec.
102 ;; -- Make enter-/event-functions anonymous.
103 ;; -- New macro `define-fsm'.
104 ;; -- Example usage in Commentary.
105
106 ;;; Code:
107
108 ;; We require cl at runtime, since we insert `destructuring-bind' into
109 ;; modules that use fsm.el.
110 (require 'cl)
111
112 (defvar fsm-debug "*fsm-debug*"
113 "*Name of buffer for fsm debug messages.
114 If nil, don't output debug messages.")
115
116 (defvar fsm-debug-timestamp-format nil
117 "*Timestamp format (a string) for `fsm-debug-output'.
118 Default format is whatever `current-time-string' returns
119 followed by a colon and a space.")
120
121 (defun fsm-debug-output (format &rest args)
122 "Append debug output to buffer named by the variable `fsm-debug'.
123 FORMAT and ARGS are passed to `format'."
124 (when fsm-debug
125 (with-current-buffer (get-buffer-create fsm-debug)
126 (save-excursion
127 (goto-char (point-max))
128 (insert (if fsm-debug-timestamp-format
129 (format-time-string fsm-debug-timestamp-format)
130 (concat (current-time-string) ": "))
131 (apply 'format format args) "\n")))))
132
133 (defmacro* define-state-machine (name &key start sleep)
134 "Define a state machine class called NAME.
135 A function called start-NAME is created, which uses the argument
136 list and body specified in the :start argument. BODY should
137 return a list of the form (STATE STATE-DATA [TIMEOUT]), where
138 STATE is the initial state (defined by `define-state'),
139 STATE-DATA is any object, and TIMEOUT is the number of seconds
140 before a :timeout event will be sent to the state machine. BODY
141 may refer to the instance being created through the dynamically
142 bound variable `fsm'.
143
144 SLEEP-FUNCTION, if provided, takes one argument, the number of
145 seconds to sleep while allowing events concerning this state
146 machine to happen. There is probably no reason to change the
147 default, which is accept-process-output with rearranged
148 arguments.
149
150 \(fn NAME :start ((ARG ...) DOCSTRING BODY) [:sleep SLEEP-FUNCTION])"
151 (declare (debug (&define name :name start
152 &rest
153 &or [":start"
154 (lambda-list
155 [&optional ("interactive" interactive)]
156 stringp def-body)]
157 [":sleep" function-form])))
158 (let ((start-name (intern (format "start-%s" name)))
159 interactive-spec)
160 (destructuring-bind (arglist docstring &body body) start
161 (when (and (consp (car body)) (eq 'interactive (caar body)))
162 (setq interactive-spec (list (pop body))))
163 (unless (stringp docstring)
164 (error "Docstring is not a string"))
165 `(progn
166 (put ',name :fsm-enter (make-hash-table :size 11 :test 'eq))
167 (put ',name :fsm-event (make-hash-table :size 11 :test 'eq))
168 (defun ,start-name ,arglist
169 ,docstring
170 ,@interactive-spec
171 (fsm-debug-output "Starting %s" ',name)
172 (let ((fsm (list :fsm ',name)))
173 (destructuring-bind (state state-data &optional timeout)
174 (progn ,@body)
175 (nconc fsm (list :state nil :state-data nil
176 :sleep ,(or sleep (lambda (secs)
177 (accept-process-output
178 nil secs)))
179 :deferred nil))
180 (fsm-update fsm state state-data timeout)
181 fsm)))))))
182
183 (defmacro* define-state (fsm-name state-name arglist &body body)
184 "Define a state called STATE-NAME in the state machine FSM-NAME.
185 ARGLIST and BODY make a function that gets called when the state
186 machine receives an event in this state. The arguments are:
187
188 FSM the state machine instance (treat it as opaque)
189 STATE-DATA An object
190 EVENT The occurred event, an object.
191 CALLBACK A function of one argument that expects the response
192 to this event, if any (often `ignore' is used)
193
194 If the event should return a response, the state machine should
195 arrange to call CALLBACK at some point in the future (not necessarily
196 in this handler).
197
198 The function should return a list of the form (NEW-STATE
199 NEW-STATE-DATA TIMEOUT):
200
201 NEW-STATE The next state, a symbol
202 NEW-STATE-DATA An object
203 TIMEOUT A number: send timeout event after this many seconds
204 nil: cancel existing timer
205 :keep: let existing timer continue
206
207 Alternatively, the function may return the keyword :defer, in
208 which case the event will be resent when the state machine enters
209 another state."
210 (declare (debug (&define name name :name handler lambda-list def-body)))
211 `(setf (gethash ',state-name (get ',fsm-name :fsm-event))
212 (lambda ,arglist ,@body)))
213
214 (defmacro* define-enter-state (fsm-name state-name arglist &body body)
215 "Define a function to call when FSM-NAME enters the state STATE-NAME.
216 ARGLIST and BODY make a function that gets called when the state
217 machine enters this state. The arguments are:
218
219 FSM the state machine instance (treat it as opaque)
220 STATE-DATA An object
221
222 The function should return a list of the form (NEW-STATE-DATA
223 TIMEOUT):
224
225 NEW-STATE-DATA An object
226 TIMEOUT A number: send timeout event after this many seconds
227 nil: cancel existing timer
228 :keep: let existing timer continue"
229 (declare (debug (&define name name :name enter lambda-list def-body)))
230 `(setf (gethash ',state-name (get ',fsm-name :fsm-enter))
231 (lambda ,arglist ,@body)))
232
233 (defmacro* define-fsm (name &key
234 start sleep states
235 (fsm-name 'fsm)
236 (state-data-name 'state-data)
237 (callback-name 'callback)
238 (event-name 'event))
239 "Define a state machine class called NAME, along with its STATES.
240 This macro is (further) syntatic sugar for `define-state-machine',
241 `define-state' and `define-enter-state' macros, q.v.
242
243 NAME is a symbol. Everything else is specified with a keyword arg.
244
245 START and SLEEP are the same as for `define-state-machine'.
246
247 STATES is a list, each element having the form (STATE-NAME . STATE-SPEC).
248 STATE-NAME is a symbol. STATE-SPEC is an alist with keys `:event' or
249 `:enter', and values a series of expressions representing the BODY of
250 a `define-state' or `define-enter-state' call, respectively.
251
252 FSM-NAME, STATE-DATA-NAME, CALLBACK-NAME, and EVENT-NAME are symbols,
253 used to construct the state functions' arglists."
254 `(progn
255 (define-state-machine ,name :start ,start :sleep ,sleep)
256 ,@(loop for (state-name . spec) in states
257 if (assq :enter spec) collect
258 `(define-enter-state ,name ,state-name
259 (,fsm-name ,state-data-name)
260 ,@(cdr it))
261 end
262 if (assq :event spec) collect
263 `(define-state ,name ,state-name
264 (,fsm-name ,state-data-name
265 ,event-name
266 ,callback-name)
267 ,@(cdr it))
268 end)))
269
270 (defun fsm-goodbye-cruel-world (name)
271 "Unbind functions related to fsm NAME (a symbol).
272 Includes start-NAME, and each fsm-NAME-STATE and fsm-NAME-enter-STATE.
273 Functions are `fmakunbound', which will probably give (fatal) pause to
274 any state machines using them. Return nil."
275 (interactive "SUnbind function definitions for fsm named: ")
276 (fmakunbound (intern (format "start-%s" name)))
277 (let (ht)
278 (when (hash-table-p (setq ht (get name :fsm-event)))
279 (clrhash ht)
280 (remprop name :fsm-event))
281 (when (hash-table-p (setq ht (get name :fsm-enter)))
282 (clrhash ht)
283 (remprop name :fsm-enter)))
284 nil)
285
286 (defun fsm-start-timer (fsm secs)
287 "Send a timeout event to FSM after SECS seconds.
288 The timer is canceled if another event occurs before, unless the
289 event handler explicitly asks to keep the timer."
290 (fsm-stop-timer fsm)
291 (setf (cddr fsm)
292 (plist-put
293 (cddr fsm)
294 :timeout (run-with-timer secs
295 nil
296 #'fsm-send-sync fsm
297 :timeout))))
298
299 (defun fsm-stop-timer (fsm)
300 "Stop the timeout timer of FSM."
301 (let ((timer (plist-get (cddr fsm) :timeout)))
302 (when (timerp timer)
303 (cancel-timer timer)
304 (setf (cddr fsm) (plist-put (cddr fsm) :timeout nil)))))
305
306 (defun fsm-maybe-change-timer (fsm timeout)
307 "Change the timer of FSM according to TIMEOUT."
308 (cond
309 ((numberp timeout)
310 (fsm-start-timer fsm timeout))
311 ((null timeout)
312 (fsm-stop-timer fsm))
313 ;; :keep needs no timer change
314 ))
315
316 (defun fsm-send (fsm event &optional callback)
317 "Send EVENT to FSM asynchronously.
318 If the state machine generates a response, eventually call
319 CALLBACK with the response as only argument."
320 (run-with-timer 0 nil #'fsm-send-sync fsm event callback))
321
322 (defun fsm-update (fsm new-state new-state-data timeout)
323 (let ((fsm-name (cadr fsm))
324 (old-state (plist-get (cddr fsm) :state)))
325 (plist-put (cddr fsm) :state new-state)
326 (plist-put (cddr fsm) :state-data new-state-data)
327 (fsm-maybe-change-timer fsm timeout)
328
329 ;; On state change, call enter function and send deferred events
330 ;; again.
331 (unless (eq old-state new-state)
332 (fsm-debug-output "%s enters %s" fsm-name new-state)
333 (let ((enter-fn (gethash new-state (get fsm-name :fsm-enter))))
334 (when (functionp enter-fn)
335 (fsm-debug-output "Found enter function for %S" new-state)
336 (condition-case e
337 (destructuring-bind (newer-state-data newer-timeout)
338 (funcall enter-fn fsm new-state-data)
339 (fsm-debug-output "Using data from enter function")
340 (plist-put (cddr fsm) :state-data newer-state-data)
341 (fsm-maybe-change-timer fsm newer-timeout))
342 ((debug error)
343 (fsm-debug-output "Didn't work: %S" e)))))
344
345 (let ((deferred (nreverse (plist-get (cddr fsm) :deferred))))
346 (setf (cddr fsm)
347 (plist-put (cddr fsm) :deferred nil))
348 (dolist (event deferred)
349 (apply 'fsm-send-sync fsm event))))))
350
351 (defun fsm-send-sync (fsm event &optional callback)
352 "Send EVENT to FSM synchronously.
353 If the state machine generates a response, eventually call
354 CALLBACK with the response as only argument."
355 (save-match-data
356 (let* ((fsm-name (second fsm))
357 (state (plist-get (cddr fsm) :state))
358 (state-data (plist-get (cddr fsm) :state-data))
359 (state-fn (gethash state (get fsm-name :fsm-event))))
360 ;; If the event is a list, output only the car, to avoid an
361 ;; overflowing debug buffer.
362 (fsm-debug-output "Sent %S to %s in state %s"
363 (or (car-safe event) event) fsm-name state)
364 (let ((result (condition-case e
365 (funcall state-fn fsm state-data event
366 (or callback 'ignore))
367 ((debug error) (cons :error-signaled e)))))
368 ;; Special case for deferring an event until next state change.
369 (cond
370 ((eq result :defer)
371 (let ((deferred (plist-get (cddr fsm) :deferred)))
372 (plist-put (cddr fsm) :deferred
373 (cons (list event callback) deferred))))
374 ((null result)
375 (fsm-debug-output "Warning: event %S ignored in state %s/%s" event fsm-name state))
376 ((eq (car-safe result) :error-signaled)
377 (fsm-debug-output "Error in %s/%s: %s"
378 fsm-name state
379 (error-message-string (cdr result))))
380 ((and (listp result)
381 (<= 2 (length result))
382 (<= (length result) 3))
383 (destructuring-bind (new-state new-state-data &optional timeout) result
384 (fsm-update fsm new-state new-state-data timeout)))
385 (t
386 (fsm-debug-output "Incorrect return value in %s/%s: %S"
387 fsm-name state
388 result)))))))
389
390 (defun fsm-call (fsm event)
391 "Send EVENT to FSM synchronously, and wait for a reply.
392 Return the reply.
393 `with-timeout' might be useful."
394 (lexical-let (reply)
395 (fsm-send-sync fsm event (lambda (r) (setq reply (list r))))
396 (while (null reply)
397 (fsm-sleep fsm 1))
398 (car reply)))
399
400 (defun fsm-make-filter (fsm)
401 "Return a filter function that sends events to FSM.
402 Events sent are of the form (:filter PROCESS STRING)."
403 (lexical-let ((fsm fsm))
404 (lambda (process string)
405 (fsm-send-sync fsm (list :filter process string)))))
406
407 (defun fsm-make-sentinel (fsm)
408 "Return a sentinel function that sends events to FSM.
409 Events sent are of the form (:sentinel PROCESS STRING)."
410 (lexical-let ((fsm fsm))
411 (lambda (process string)
412 (fsm-send-sync fsm (list :sentinel process string)))))
413
414 (defun fsm-sleep (fsm secs)
415 "Sleep up to SECS seconds in a way that lets FSM receive events."
416 (funcall (plist-get (cddr fsm) :sleep) secs))
417
418 (defun fsm-get-state-data (fsm)
419 "Return the state data of FSM.
420 Note the absence of a set function. The fsm should manage its
421 state data itself; other code should just send messages to it."
422 (plist-get (cddr fsm) :state-data))
423
424 (provide 'fsm)
425
426 ;;; fsm.el ends here