1 ;;; fsm.el --- state machine library -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2006, 2007, 2008, 2015 Free Software Foundation, Inc.
5 ;; Author: Magnus Henoch <magnus.henoch@gmail.com>
6 ;; Maintainer: Thomas Fitzsimmons <fitzsim@fitzsim.org>
8 ;; Package-Requires: ((emacs "24.1") (cl-lib "0.5"))
9 ;; Keywords: extensions
11 ;; This file is free software; you can redistribute it and/or modify
12 ;; it under the terms of the GNU General Public License as published by
13 ;; the Free Software Foundation; either version 2, or (at your option)
16 ;; This file is distributed in the hope that it will be useful,
17 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
18 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19 ;; GNU General Public License for more details.
21 ;; You should have received a copy of the GNU General Public License
22 ;; along with GNU Emacs; see the file COPYING. If not, write to
23 ;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
24 ;; Boston, MA 02110-1301, USA.
28 ;; fsm.el is an exercise in metaprogramming inspired by gen_fsm of
29 ;; Erlang/OTP. It aims to make asynchronous programming in Emacs Lisp
30 ;; easy and fun. By "asynchronous" I mean that long-lasting tasks
31 ;; don't interfer with normal editing.
33 ;; Some people say that it would be nice if Emacs Lisp had threads
34 ;; and/or continuations. They are probably right, but there are few
35 ;; things that can't be made to run in the background using facilities
36 ;; already available: timers, filters and sentinels. As the code can
37 ;; become a bit messy when using such means, with callbacks everywhere
38 ;; and such things, it can be useful to structure the program as a
41 ;; In this model, a state machine passes between different "states",
42 ;; which are actually only different event handler functions. The
43 ;; state machine receives "events" (from timers, filters, user
44 ;; requests, etc) and reacts to them, possibly entering another state,
45 ;; possibly returning a value.
47 ;; The essential macros/functions are:
49 ;; define-state-machine - create start-FOO function
50 ;; define-state - event handler for each state (required)
51 ;; define-enter-state - called when entering a state (optional)
52 ;; define-fsm - encapsulates the above three (more sugar!)
53 ;; fsm-send - send an event to a state machine
54 ;; fsm-call - send an event and wait for reply
56 ;; fsm.el is similar to but different from Distel:
57 ;; <URL:http://fresh.homeunix.net/~luke/distel/>
58 ;; Emacs' tq library is a similar idea.
60 ;; Here is a simple (not using all the features of fsm.el) example:
62 ;; ;; -*- lexical-binding: t; -*-
64 ;; (cl-labels ((hey (n ev)
65 ;; (message "%d (%s)\tp%sn%s!" n ev
66 ;; (if (zerop (% n 4)) "o" "i")
67 ;; (make-string (max 1 (abs n)) ?g))))
68 ;; (cl-macrolet ((zow (next timeout)
69 ;; `(progn (hey (cl-incf count) event)
70 ;; (list ,next count ,timeout))))
71 ;; (define-fsm pingpong
72 ;; :start ((init) "Start a pingpong fsm."
73 ;; (interactive "nInit (number, negative to auto-terminate): ")
74 ;; (list :ping (ash (ash init -2) 2) ; 4 is death
75 ;; (when (interactive-p) 0)))
76 ;; :state-data-name count
79 ;; (:event (zow :pingg 0.1)))
81 ;; (:event (zow :pinggg 0.1)))
83 ;; (:event (zow :pong 1)))
85 ;; (:event (zow :ping (if (= 0 count)
86 ;; (fsm-goodbye-cruel-world 'pingpong)
88 ;; (fsm-send (start-pingpong -16) t)
90 ;; Copy into a buffer, uncomment, and type M-x eval-buffer RET.
91 ;; Alternatively, you can replace the `fsm-goodbye-cruel-world'
92 ;; form with `nil', eval just the `cl-labels' form and then type
93 ;; M-x start-pingpong RET -16 RET.
96 ;; -- Delete trailing whitespace.
98 ;; -- Use lexical binding.
100 ;; -- Remove unnecessary fsm-debug-output message.
101 ;; -- Add FSM name to fsm-debug-output messages that were not including it.
102 ;; -- Fix checkdoc errors.
103 ;; -- Change FSMs from plists to uninterned symbols.
105 ;; NOTE: This is version 0.1ttn4 of fsm.el, with the following
106 ;; mods (an exercise in meta-meta-programming ;-) by ttn:
107 ;; -- Refill for easy (traditional 80-column) perusal.
108 ;; -- New var `fsm-debug-timestamp-format'.
109 ;; -- Make variables satisfy `user-variable-p'.
110 ;; -- Use `format' instead of `concat'.
111 ;; -- New func `fsm-goodbye-cruel-world'.
112 ;; -- Make start-function respect `interactive' spec.
113 ;; -- Make enter-/event-functions anonymous.
114 ;; -- New macro `define-fsm'.
115 ;; -- Example usage in Commentary.
119 ;; We require cl-lib at runtime, since we insert `cl-destructuring-bind' into
120 ;; modules that use fsm.el.
123 (defvar fsm-debug "*fsm-debug*"
124 "*Name of buffer for fsm debug messages.
125 If nil, don't output debug messages.")
127 (defvar fsm-debug-timestamp-format nil
128 "*Timestamp format (a string) for `fsm-debug-output'.
129 Default format is whatever `current-time-string' returns
130 followed by a colon and a space.")
132 (defun fsm-debug-output (format &rest args)
133 "Append debug output to buffer named by the variable `fsm-debug'.
134 FORMAT and ARGS are passed to `format'."
136 (with-current-buffer (get-buffer-create fsm-debug)
138 (goto-char (point-max))
139 (insert (if fsm-debug-timestamp-format
140 (format-time-string fsm-debug-timestamp-format)
141 (concat (current-time-string) ": "))
142 (apply 'format format args) "\n")))))
144 (cl-defmacro define-state-machine (name &key start sleep)
145 "Define a state machine class called NAME.
146 A function called start-NAME is created, which uses the argument
147 list and body specified in the :start argument. BODY should
148 return a list of the form (STATE STATE-DATA [TIMEOUT]), where
149 STATE is the initial state (defined by `define-state'),
150 STATE-DATA is any object, and TIMEOUT is the number of seconds
151 before a :timeout event will be sent to the state machine. BODY
152 may refer to the instance being created through the dynamically
153 bound variable `fsm'.
155 SLEEP-FUNCTION, if provided, takes one argument, the number of
156 seconds to sleep while allowing events concerning this state
157 machine to happen. There is probably no reason to change the
158 default, which is accept-process-output with rearranged
161 \(fn NAME :start ((ARG ...) DOCSTRING BODY) [:sleep SLEEP-FUNCTION])"
162 (declare (debug (&define name :name start
166 [&optional ("interactive" interactive)]
168 [":sleep" function-form])))
169 (let ((start-name (intern (format "start-%s" name)))
171 (cl-destructuring-bind (arglist docstring &body body) start
172 (when (and (consp (car body)) (eq 'interactive (caar body)))
173 (setq interactive-spec (list (pop body))))
174 (unless (stringp docstring)
175 (error "Docstring is not a string"))
177 (put ',name :fsm-enter (make-hash-table :size 11 :test 'eq))
178 (put ',name :fsm-event (make-hash-table :size 11 :test 'eq))
179 (defun ,start-name ,arglist
182 (fsm-debug-output "Starting %s" ',name)
183 (let ((fsm (cl-gensym (concat "fsm-" ,(symbol-name name) "-"))))
184 (cl-destructuring-bind (state state-data &optional timeout)
186 (put fsm :name ',name)
188 (put fsm :state-data nil)
189 (put fsm :sleep ,(or sleep '(lambda (secs)
190 (accept-process-output
193 (put fsm :deferred nil)
194 (fsm-update fsm state state-data timeout)
197 (cl-defmacro define-state (fsm-name state-name arglist &body body)
198 "Define a state called STATE-NAME in the state machine FSM-NAME.
199 ARGLIST and BODY make a function that gets called when the state
200 machine receives an event in this state. The arguments are:
202 FSM the state machine instance (treat it as opaque)
204 EVENT The occurred event, an object.
205 CALLBACK A function of one argument that expects the response
206 to this event, if any (often `ignore' is used)
208 If the event should return a response, the state machine should
209 arrange to call CALLBACK at some point in the future (not necessarily
212 The function should return a list of the form (NEW-STATE
213 NEW-STATE-DATA TIMEOUT):
215 NEW-STATE The next state, a symbol
216 NEW-STATE-DATA An object
217 TIMEOUT A number: send timeout event after this many seconds
218 nil: cancel existing timer
219 :keep: let existing timer continue
221 Alternatively, the function may return the keyword :defer, in
222 which case the event will be resent when the state machine enters
224 (declare (debug (&define name name :name handler lambda-list def-body)))
225 `(setf (gethash ',state-name (get ',fsm-name :fsm-event))
226 (lambda ,arglist ,@body)))
228 (cl-defmacro define-enter-state (fsm-name state-name arglist &body body)
229 "Define a function to call when FSM-NAME enters the state STATE-NAME.
230 ARGLIST and BODY make a function that gets called when the state
231 machine enters this state. The arguments are:
233 FSM the state machine instance (treat it as opaque)
236 The function should return a list of the form (NEW-STATE-DATA
239 NEW-STATE-DATA An object
240 TIMEOUT A number: send timeout event after this many seconds
241 nil: cancel existing timer
242 :keep: let existing timer continue"
243 (declare (debug (&define name name :name enter lambda-list def-body)))
244 `(setf (gethash ',state-name (get ',fsm-name :fsm-enter))
245 (lambda ,arglist ,@body)))
247 (cl-defmacro define-fsm (name &key
250 (state-data-name 'state-data)
251 (callback-name 'callback)
253 "Define a state machine class called NAME, along with its STATES.
254 This macro is (further) syntatic sugar for `define-state-machine',
255 `define-state' and `define-enter-state' macros, q.v.
257 NAME is a symbol. Everything else is specified with a keyword arg.
259 START and SLEEP are the same as for `define-state-machine'.
261 STATES is a list, each element having the form (STATE-NAME . STATE-SPEC).
262 STATE-NAME is a symbol. STATE-SPEC is an alist with keys `:event' or
263 `:enter', and values a series of expressions representing the BODY of
264 a `define-state' or `define-enter-state' call, respectively.
266 FSM-NAME, STATE-DATA-NAME, CALLBACK-NAME, and EVENT-NAME are symbols,
267 used to construct the state functions' arglists."
269 (define-state-machine ,name :start ,start :sleep ,sleep)
270 ,@(cl-loop for (state-name . spec) in states
271 if (assq :enter spec) collect
272 `(define-enter-state ,name ,state-name
273 (,fsm-name ,state-data-name)
276 if (assq :event spec) collect
277 `(define-state ,name ,state-name
278 (,fsm-name ,state-data-name
284 (defun fsm-goodbye-cruel-world (name)
285 "Unbind functions related to fsm NAME (a symbol).
286 Includes start-NAME, and each fsm-NAME-STATE and fsm-NAME-enter-STATE.
287 Functions are `fmakunbound', which will probably give (fatal) pause to
288 any state machines using them. Return nil."
289 (interactive "SUnbind function definitions for fsm named: ")
290 (fmakunbound (intern (format "start-%s" name)))
292 (when (hash-table-p (setq ht (get name :fsm-event)))
294 (cl-remprop name :fsm-event))
295 (when (hash-table-p (setq ht (get name :fsm-enter)))
297 (cl-remprop name :fsm-enter)))
300 (defun fsm-start-timer (fsm secs)
301 "Send a timeout event to FSM after SECS seconds.
302 The timer is canceled if another event occurs before, unless the
303 event handler explicitly asks to keep the timer."
306 :timeout (run-with-timer
308 #'fsm-send-sync fsm :timeout)))
310 (defun fsm-stop-timer (fsm)
311 "Stop the timeout timer of FSM."
312 (let ((timer (get fsm :timeout)))
315 (put fsm :timeout nil))))
317 (defun fsm-maybe-change-timer (fsm timeout)
318 "Change the timer of FSM according to TIMEOUT."
321 (fsm-start-timer fsm timeout))
323 (fsm-stop-timer fsm))
324 ;; :keep needs no timer change
327 (defun fsm-send (fsm event &optional callback)
328 "Send EVENT to FSM asynchronously.
329 If the state machine generates a response, eventually call
330 CALLBACK with the response as only argument."
331 (run-with-timer 0 nil #'fsm-send-sync fsm event callback))
333 (defun fsm-update (fsm new-state new-state-data timeout)
334 "Update FSM with NEW-STATE, NEW-STATE-DATA and TIMEOUT."
335 (let ((fsm-name (get fsm :name))
336 (old-state (get fsm :state)))
337 (put fsm :state new-state)
338 (put fsm :state-data new-state-data)
339 (fsm-maybe-change-timer fsm timeout)
341 ;; On state change, call enter function and send deferred events
343 (unless (eq old-state new-state)
344 (fsm-debug-output "%s enters %s" fsm-name new-state)
345 (let ((enter-fn (gethash new-state (get fsm-name :fsm-enter))))
346 (when (functionp enter-fn)
347 (fsm-debug-output "Found enter function for %s/%s" fsm-name new-state)
349 (cl-destructuring-bind (newer-state-data newer-timeout)
350 (funcall enter-fn fsm new-state-data)
351 (put fsm :state-data newer-state-data)
352 (fsm-maybe-change-timer fsm newer-timeout))
354 (fsm-debug-output "%s/%s update didn't work: %S"
355 fsm-name new-state e)))))
357 (let ((deferred (nreverse (get fsm :deferred))))
358 (put fsm :deferred nil)
359 (dolist (event deferred)
360 (apply 'fsm-send-sync fsm event))))))
362 (defun fsm-send-sync (fsm event &optional callback)
363 "Send EVENT to FSM synchronously.
364 If the state machine generates a response, eventually call
365 CALLBACK with the response as only argument."
367 (let* ((fsm-name (get fsm :name))
368 (state (get fsm :state))
369 (state-data (get fsm :state-data))
370 (state-fn (gethash state (get fsm-name :fsm-event))))
371 ;; If the event is a list, output only the car, to avoid an
372 ;; overflowing debug buffer.
373 (fsm-debug-output "Sent %S to %s in state %s"
374 (or (car-safe event) event) fsm-name state)
375 (let ((result (condition-case e
376 (funcall state-fn fsm state-data event
377 (or callback 'ignore))
378 ((debug error) (cons :error-signaled e)))))
379 ;; Special case for deferring an event until next state change.
382 (let ((deferred (get fsm :deferred)))
383 (put fsm :deferred (cons (list event callback) deferred))))
385 (fsm-debug-output "Warning: event %S ignored in state %s/%s"
386 event fsm-name state))
387 ((eq (car-safe result) :error-signaled)
388 (fsm-debug-output "Error in %s/%s: %s"
390 (error-message-string (cdr result))))
392 (<= 2 (length result))
393 (<= (length result) 3))
394 (cl-destructuring-bind (new-state new-state-data &optional timeout)
396 (fsm-update fsm new-state new-state-data timeout)))
398 (fsm-debug-output "Incorrect return value in %s/%s: %S"
402 (defun fsm-call (fsm event)
403 "Send EVENT to FSM synchronously, and wait for a reply.
404 Return the reply. `with-timeout' might be useful."
406 (fsm-send-sync fsm event (lambda (r) (setq reply (list r))))
411 (defun fsm-make-filter (fsm)
412 "Return a filter function that sends events to FSM.
413 Events sent are of the form (:filter PROCESS STRING)."
415 (lambda (process string)
416 (fsm-send-sync fsm (list :filter process string)))))
418 (defun fsm-make-sentinel (fsm)
419 "Return a sentinel function that sends events to FSM.
420 Events sent are of the form (:sentinel PROCESS STRING)."
422 (lambda (process string)
423 (fsm-send-sync fsm (list :sentinel process string)))))
425 (defun fsm-sleep (fsm secs)
426 "Sleep up to SECS seconds in a way that lets FSM receive events."
427 (funcall (get fsm :sleep) secs))
429 (defun fsm-get-state-data (fsm)
430 "Return the state data of FSM.
431 Note the absence of a set function. The fsm should manage its
432 state data itself; other code should just send messages to it."
433 (get fsm :state-data))
437 ;; 2016-07-10 Thomas Fitzsimmons <fitzsim@fitzsim.org>
439 ;; packages/fsm: Bump version to 0.2.1
441 ;; 2016-07-10 Thomas Fitzsimmons <fitzsim@fitzsim.org>
443 ;; packages/fsm: Fix compilation error
445 ;; * packages/fsm/fsm.el (define-state-machine): Quote default :sleep
446 ;; lambda (bug#23920).
448 ;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
450 ;; fsm: Revert some changes suggested by checkdoc
452 ;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
454 ;; fsm: Bump version to 0.2
456 ;; 2015-09-05 Magnus Henoch <magnus.henoch@gmail.com>
458 ;; fsm: Change FSMs from plists to uninterned symbols
460 ;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
462 ;; fsm: Fix copyright
464 ;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
466 ;; fsm: Add packaging fields
468 ;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
470 ;; fsm: Fix checkdoc errors
472 ;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
474 ;; fsm: Add FSM name to some fsm-debug-output messages
476 ;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
478 ;; fsm: Port to cl-lib
480 ;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
482 ;; fsm: Use lexical binding
484 ;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
486 ;; fsm: Fix formatting
488 ;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
490 ;; fsm: Delete trailing whitespace
492 ;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
494 ;; fsm: Import fsm.el from emacs-jabber
496 ;; Import fsm.el from git://git.code.sf.net/p/emacs-jabber/git, commit
497 ;; 1f858cc4f3cdabcd7380a7d08af273bcdd708c15.