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