]> code.delx.au - gnu-emacs-elpa/blob - packages/fsm/fsm.el
4bc4ebfb8b89976b657e3699f958be69c4a02b99
[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 ;;
85 ;; (fsm-send (start-pingpong -16) t)
86 ;;
87 ;; Copy into a buffer, uncomment, and type M-x eval-buffer RET.
88 ;; Alternatively, you can replace the `fsm-goodbye-cruel-world'
89 ;; form with `nil', eval just the `labels' form and then type
90 ;; M-x start-pingpong RET -16 RET.
91
92 ;; NOTE: This is version 0.1ttn4 of fsm.el, with the following
93 ;; mods (an exercise in meta-meta-programming ;-) by ttn:
94 ;; -- Refill for easy (traditional 80-column) perusal.
95 ;; -- New var `fsm-debug-timestamp-format'.
96 ;; -- Make variables satisfy `user-variable-p'.
97 ;; -- Use `format' instead of `concat'.
98 ;; -- New func `fsm-goodbye-cruel-world'.
99 ;; -- Make start-function respect `interactive' spec.
100 ;; -- Make enter-/event-functions anonymous.
101 ;; -- New macro `define-fsm'.
102 ;; -- Example usage in Commentary.
103
104 ;;; Code:
105
106 ;; We require cl at runtime, since we insert `destructuring-bind' into
107 ;; modules that use fsm.el.
108 (require 'cl)
109
110 (defvar fsm-debug "*fsm-debug*"
111 "*Name of buffer for fsm debug messages.
112 If nil, don't output debug messages.")
113
114 (defvar fsm-debug-timestamp-format nil
115 "*Timestamp format (a string) for `fsm-debug-output'.
116 Default format is whatever `current-time-string' returns
117 followed by a colon and a space.")
118
119 (defun fsm-debug-output (format &rest args)
120 "Append debug output to buffer named by the variable `fsm-debug'.
121 FORMAT and ARGS are passed to `format'."
122 (when fsm-debug
123 (with-current-buffer (get-buffer-create fsm-debug)
124 (save-excursion
125 (goto-char (point-max))
126 (insert (if fsm-debug-timestamp-format
127 (format-time-string fsm-debug-timestamp-format)
128 (concat (current-time-string) ": "))
129 (apply 'format format args) "\n")))))
130
131 (defmacro* define-state-machine (name &key start sleep)
132 "Define a state machine class called NAME.
133 A function called start-NAME is created, which uses the argument
134 list and body specified in the :start argument. BODY should
135 return a list of the form (STATE STATE-DATA [TIMEOUT]), where
136 STATE is the initial state (defined by `define-state'),
137 STATE-DATA is any object, and TIMEOUT is the number of seconds
138 before a :timeout event will be sent to the state machine. BODY
139 may refer to the instance being created through the dynamically
140 bound variable `fsm'.
141
142 SLEEP-FUNCTION, if provided, takes one argument, the number of
143 seconds to sleep while allowing events concerning this state
144 machine to happen. There is probably no reason to change the
145 default, which is accept-process-output with rearranged
146 arguments.
147
148 \(fn NAME :start ((ARG ...) DOCSTRING BODY) [:sleep SLEEP-FUNCTION])"
149 (declare (debug (&define name :name start
150 &rest
151 &or [":start"
152 (lambda-list
153 [&optional ("interactive" interactive)]
154 stringp def-body)]
155 [":sleep" function-form])))
156 (let ((start-name (intern (format "start-%s" name)))
157 interactive-spec)
158 (destructuring-bind (arglist docstring &body body) start
159 (when (and (consp (car body)) (eq 'interactive (caar body)))
160 (setq interactive-spec (list (pop body))))
161 (unless (stringp docstring)
162 (error "Docstring is not a string"))
163 `(progn
164 (put ',name :fsm-enter (make-hash-table :size 11 :test 'eq))
165 (put ',name :fsm-event (make-hash-table :size 11 :test 'eq))
166 (defun ,start-name ,arglist
167 ,docstring
168 ,@interactive-spec
169 (fsm-debug-output "Starting %s" ',name)
170 (let ((fsm (list :fsm ',name)))
171 (destructuring-bind (state state-data &optional timeout)
172 (progn ,@body)
173 (nconc fsm (list :state nil :state-data nil
174 :sleep ,(or sleep (lambda (secs)
175 (accept-process-output
176 nil secs)))
177 :deferred nil))
178 (fsm-update fsm state state-data timeout)
179 fsm)))))))
180
181 (defmacro* define-state (fsm-name state-name arglist &body body)
182 "Define a state called STATE-NAME in the state machine FSM-NAME.
183 ARGLIST and BODY make a function that gets called when the state
184 machine receives an event in this state. The arguments are:
185
186 FSM the state machine instance (treat it as opaque)
187 STATE-DATA An object
188 EVENT The occurred event, an object.
189 CALLBACK A function of one argument that expects the response
190 to this event, if any (often `ignore' is used)
191
192 If the event should return a response, the state machine should
193 arrange to call CALLBACK at some point in the future (not necessarily
194 in this handler).
195
196 The function should return a list of the form (NEW-STATE
197 NEW-STATE-DATA TIMEOUT):
198
199 NEW-STATE The next state, a symbol
200 NEW-STATE-DATA An object
201 TIMEOUT A number: send timeout event after this many seconds
202 nil: cancel existing timer
203 :keep: let existing timer continue
204
205 Alternatively, the function may return the keyword :defer, in
206 which case the event will be resent when the state machine enters
207 another state."
208 (declare (debug (&define name name :name handler lambda-list def-body)))
209 `(setf (gethash ',state-name (get ',fsm-name :fsm-event))
210 (lambda ,arglist ,@body)))
211
212 (defmacro* define-enter-state (fsm-name state-name arglist &body body)
213 "Define a function to call when FSM-NAME enters the state STATE-NAME.
214 ARGLIST and BODY make a function that gets called when the state
215 machine enters this state. The arguments are:
216
217 FSM the state machine instance (treat it as opaque)
218 STATE-DATA An object
219
220 The function should return a list of the form (NEW-STATE-DATA
221 TIMEOUT):
222
223 NEW-STATE-DATA An object
224 TIMEOUT A number: send timeout event after this many seconds
225 nil: cancel existing timer
226 :keep: let existing timer continue"
227 (declare (debug (&define name name :name enter lambda-list def-body)))
228 `(setf (gethash ',state-name (get ',fsm-name :fsm-enter))
229 (lambda ,arglist ,@body)))
230
231 (defmacro* define-fsm (name &key
232 start sleep states
233 (fsm-name 'fsm)
234 (state-data-name 'state-data)
235 (callback-name 'callback)
236 (event-name 'event))
237 "Define a state machine class called NAME, along with its STATES.
238 This macro is (further) syntatic sugar for `define-state-machine',
239 `define-state' and `define-enter-state' macros, q.v.
240
241 NAME is a symbol. Everything else is specified with a keyword arg.
242
243 START and SLEEP are the same as for `define-state-machine'.
244
245 STATES is a list, each element having the form (STATE-NAME . STATE-SPEC).
246 STATE-NAME is a symbol. STATE-SPEC is an alist with keys `:event' or
247 `:enter', and values a series of expressions representing the BODY of
248 a `define-state' or `define-enter-state' call, respectively.
249
250 FSM-NAME, STATE-DATA-NAME, CALLBACK-NAME, and EVENT-NAME are symbols,
251 used to construct the state functions' arglists."
252 `(progn
253 (define-state-machine ,name :start ,start :sleep ,sleep)
254 ,@(loop for (state-name . spec) in states
255 if (assq :enter spec) collect
256 `(define-enter-state ,name ,state-name
257 (,fsm-name ,state-data-name)
258 ,@(cdr it))
259 end
260 if (assq :event spec) collect
261 `(define-state ,name ,state-name
262 (,fsm-name ,state-data-name
263 ,event-name
264 ,callback-name)
265 ,@(cdr it))
266 end)))
267
268 (defun fsm-goodbye-cruel-world (name)
269 "Unbind functions related to fsm NAME (a symbol).
270 Includes start-NAME, and each fsm-NAME-STATE and fsm-NAME-enter-STATE.
271 Functions are `fmakunbound', which will probably give (fatal) pause to
272 any state machines using them. Return nil."
273 (interactive "SUnbind function definitions for fsm named: ")
274 (fmakunbound (intern (format "start-%s" name)))
275 (let (ht)
276 (when (hash-table-p (setq ht (get name :fsm-event)))
277 (clrhash ht)
278 (remprop name :fsm-event))
279 (when (hash-table-p (setq ht (get name :fsm-enter)))
280 (clrhash ht)
281 (remprop name :fsm-enter)))
282 nil)
283
284 (defun fsm-start-timer (fsm secs)
285 "Send a timeout event to FSM after SECS seconds.
286 The timer is canceled if another event occurs before, unless the
287 event handler explicitly asks to keep the timer."
288 (fsm-stop-timer fsm)
289 (setf (cddr fsm)
290 (plist-put
291 (cddr fsm)
292 :timeout (run-with-timer secs
293 nil
294 #'fsm-send-sync fsm
295 :timeout))))
296
297 (defun fsm-stop-timer (fsm)
298 "Stop the timeout timer of FSM."
299 (let ((timer (plist-get (cddr fsm) :timeout)))
300 (when (timerp timer)
301 (cancel-timer timer)
302 (setf (cddr fsm) (plist-put (cddr fsm) :timeout nil)))))
303
304 (defun fsm-maybe-change-timer (fsm timeout)
305 "Change the timer of FSM according to TIMEOUT."
306 (cond
307 ((numberp timeout)
308 (fsm-start-timer fsm timeout))
309 ((null timeout)
310 (fsm-stop-timer fsm))
311 ;; :keep needs no timer change
312 ))
313
314 (defun fsm-send (fsm event &optional callback)
315 "Send EVENT to FSM asynchronously.
316 If the state machine generates a response, eventually call
317 CALLBACK with the response as only argument."
318 (run-with-timer 0 nil #'fsm-send-sync fsm event callback))
319
320 (defun fsm-update (fsm new-state new-state-data timeout)
321 (let ((fsm-name (cadr fsm))
322 (old-state (plist-get (cddr fsm) :state)))
323 (plist-put (cddr fsm) :state new-state)
324 (plist-put (cddr fsm) :state-data new-state-data)
325 (fsm-maybe-change-timer fsm timeout)
326
327 ;; On state change, call enter function and send deferred events
328 ;; again.
329 (unless (eq old-state new-state)
330 (fsm-debug-output "%s enters %s" fsm-name new-state)
331 (let ((enter-fn (gethash new-state (get fsm-name :fsm-enter))))
332 (when (functionp enter-fn)
333 (fsm-debug-output "Found enter function for %S" new-state)
334 (condition-case e
335 (destructuring-bind (newer-state-data newer-timeout)
336 (funcall enter-fn fsm new-state-data)
337 (fsm-debug-output "Using data from enter function")
338 (plist-put (cddr fsm) :state-data newer-state-data)
339 (fsm-maybe-change-timer fsm newer-timeout))
340 ((debug error)
341 (fsm-debug-output "Didn't work: %S" e)))))
342
343 (let ((deferred (nreverse (plist-get (cddr fsm) :deferred))))
344 (setf (cddr fsm)
345 (plist-put (cddr fsm) :deferred nil))
346 (dolist (event deferred)
347 (apply 'fsm-send-sync fsm event))))))
348
349 (defun fsm-send-sync (fsm event &optional callback)
350 "Send EVENT to FSM synchronously.
351 If the state machine generates a response, eventually call
352 CALLBACK with the response as only argument."
353 (save-match-data
354 (let* ((fsm-name (second fsm))
355 (state (plist-get (cddr fsm) :state))
356 (state-data (plist-get (cddr fsm) :state-data))
357 (state-fn (gethash state (get fsm-name :fsm-event))))
358 ;; If the event is a list, output only the car, to avoid an
359 ;; overflowing debug buffer.
360 (fsm-debug-output "Sent %S to %s in state %s"
361 (or (car-safe event) event) fsm-name state)
362 (let ((result (condition-case e
363 (funcall state-fn fsm state-data event
364 (or callback 'ignore))
365 ((debug error) (cons :error-signaled e)))))
366 ;; Special case for deferring an event until next state change.
367 (cond
368 ((eq result :defer)
369 (let ((deferred (plist-get (cddr fsm) :deferred)))
370 (plist-put (cddr fsm) :deferred
371 (cons (list event callback) deferred))))
372 ((null result)
373 (fsm-debug-output "Warning: event %S ignored in state %s/%s" event fsm-name state))
374 ((eq (car-safe result) :error-signaled)
375 (fsm-debug-output "Error in %s/%s: %s"
376 fsm-name state
377 (error-message-string (cdr result))))
378 ((and (listp result)
379 (<= 2 (length result))
380 (<= (length result) 3))
381 (destructuring-bind (new-state new-state-data &optional timeout) result
382 (fsm-update fsm new-state new-state-data timeout)))
383 (t
384 (fsm-debug-output "Incorrect return value in %s/%s: %S"
385 fsm-name state
386 result)))))))
387
388 (defun fsm-call (fsm event)
389 "Send EVENT to FSM synchronously, and wait for a reply.
390 Return the reply.
391 `with-timeout' might be useful."
392 (lexical-let (reply)
393 (fsm-send-sync fsm event (lambda (r) (setq reply (list r))))
394 (while (null reply)
395 (fsm-sleep fsm 1))
396 (car reply)))
397
398 (defun fsm-make-filter (fsm)
399 "Return a filter function that sends events to FSM.
400 Events sent are of the form (:filter PROCESS STRING)."
401 (lexical-let ((fsm fsm))
402 (lambda (process string)
403 (fsm-send-sync fsm (list :filter process string)))))
404
405 (defun fsm-make-sentinel (fsm)
406 "Return a sentinel function that sends events to FSM.
407 Events sent are of the form (:sentinel PROCESS STRING)."
408 (lexical-let ((fsm fsm))
409 (lambda (process string)
410 (fsm-send-sync fsm (list :sentinel process string)))))
411
412 (defun fsm-sleep (fsm secs)
413 "Sleep up to SECS seconds in a way that lets FSM receive events."
414 (funcall (plist-get (cddr fsm) :sleep) secs))
415
416 (defun fsm-get-state-data (fsm)
417 "Return the state data of FSM.
418 Note the absence of a set function. The fsm should manage its
419 state data itself; other code should just send messages to it."
420 (plist-get (cddr fsm) :state-data))
421
422 (provide 'fsm)
423
424 ;;; fsm.el ends here