]> crepu.dev Git - config.git/blame - djavu-asus/emacs/elpa/fsm-0.2.1/fsm.el
Reorganización de directorios
[config.git] / djavu-asus / emacs / elpa / fsm-0.2.1 / fsm.el
CommitLineData
53e6db90
DC
1;;; fsm.el --- state machine library -*- lexical-binding: t; -*-
2
3;; Copyright (C) 2006, 2007, 2008, 2015 Free Software Foundation, Inc.
4
5;; Author: Magnus Henoch <magnus.henoch@gmail.com>
6;; Maintainer: Thomas Fitzsimmons <fitzsim@fitzsim.org>
7;; Version: 0.2.1
8;; Package-Requires: ((emacs "24.1") (cl-lib "0.5"))
9;; Keywords: extensions
10
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)
14;; any later version.
15
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.
20
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.
25
26;;; Commentary:
27
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.
32
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
39;; state machine.
40
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.
46
47;; The essential macros/functions are:
48;;
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
55
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.
59
60;; Here is a simple (not using all the features of fsm.el) example:
61;;
62;; ;; -*- lexical-binding: t; -*-
63;; (require 'fsm)
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
77;; :states
78;; ((:ping
79;; (:event (zow :pingg 0.1)))
80;; (:pingg
81;; (:event (zow :pinggg 0.1)))
82;; (:pinggg
83;; (:event (zow :pong 1)))
84;; (:pong
85;; (:event (zow :ping (if (= 0 count)
86;; (fsm-goodbye-cruel-world 'pingpong)
87;; 3))))))))
88;; (fsm-send (start-pingpong -16) t)
89;;
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.
94
95;; Version 0.2:
96;; -- Delete trailing whitespace.
97;; -- Fix formatting.
98;; -- Use lexical binding.
99;; -- Port to cl-lib.
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.
104
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.
116
117;;; Code:
118
119;; We require cl-lib at runtime, since we insert `cl-destructuring-bind' into
120;; modules that use fsm.el.
121(require 'cl-lib)
122
123(defvar fsm-debug "*fsm-debug*"
124 "*Name of buffer for fsm debug messages.
125If nil, don't output debug messages.")
126
127(defvar fsm-debug-timestamp-format nil
128 "*Timestamp format (a string) for `fsm-debug-output'.
129Default format is whatever `current-time-string' returns
130followed by a colon and a space.")
131
132(defun fsm-debug-output (format &rest args)
133 "Append debug output to buffer named by the variable `fsm-debug'.
134FORMAT and ARGS are passed to `format'."
135 (when fsm-debug
136 (with-current-buffer (get-buffer-create fsm-debug)
137 (save-excursion
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")))))
143
144(cl-defmacro define-state-machine (name &key start sleep)
145 "Define a state machine class called NAME.
146A function called start-NAME is created, which uses the argument
147list and body specified in the :start argument. BODY should
148return a list of the form (STATE STATE-DATA [TIMEOUT]), where
149STATE is the initial state (defined by `define-state'),
150STATE-DATA is any object, and TIMEOUT is the number of seconds
151before a :timeout event will be sent to the state machine. BODY
152may refer to the instance being created through the dynamically
153bound variable `fsm'.
154
155SLEEP-FUNCTION, if provided, takes one argument, the number of
156seconds to sleep while allowing events concerning this state
157machine to happen. There is probably no reason to change the
158default, which is accept-process-output with rearranged
159arguments.
160
161\(fn NAME :start ((ARG ...) DOCSTRING BODY) [:sleep SLEEP-FUNCTION])"
162 (declare (debug (&define name :name start
163 &rest
164 &or [":start"
165 (lambda-list
166 [&optional ("interactive" interactive)]
167 stringp def-body)]
168 [":sleep" function-form])))
169 (let ((start-name (intern (format "start-%s" name)))
170 interactive-spec)
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"))
176 `(progn
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
180 ,docstring
181 ,@interactive-spec
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)
185 (progn ,@body)
186 (put fsm :name ',name)
187 (put fsm :state nil)
188 (put fsm :state-data nil)
189 (put fsm :sleep ,(or sleep '(lambda (secs)
190 (accept-process-output
191 nil secs))))
192
193 (put fsm :deferred nil)
194 (fsm-update fsm state state-data timeout)
195 fsm)))))))
196
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.
199ARGLIST and BODY make a function that gets called when the state
200machine receives an event in this state. The arguments are:
201
202FSM the state machine instance (treat it as opaque)
203STATE-DATA An object
204EVENT The occurred event, an object.
205CALLBACK A function of one argument that expects the response
206 to this event, if any (often `ignore' is used)
207
208If the event should return a response, the state machine should
209arrange to call CALLBACK at some point in the future (not necessarily
210in this handler).
211
212The function should return a list of the form (NEW-STATE
213NEW-STATE-DATA TIMEOUT):
214
215NEW-STATE The next state, a symbol
216NEW-STATE-DATA An object
217TIMEOUT A number: send timeout event after this many seconds
218 nil: cancel existing timer
219 :keep: let existing timer continue
220
221Alternatively, the function may return the keyword :defer, in
222which case the event will be resent when the state machine enters
223another state."
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)))
227
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.
230ARGLIST and BODY make a function that gets called when the state
231machine enters this state. The arguments are:
232
233FSM the state machine instance (treat it as opaque)
234STATE-DATA An object
235
236The function should return a list of the form (NEW-STATE-DATA
237TIMEOUT):
238
239NEW-STATE-DATA An object
240TIMEOUT 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)))
246
247(cl-defmacro define-fsm (name &key
248 start sleep states
249 (fsm-name 'fsm)
250 (state-data-name 'state-data)
251 (callback-name 'callback)
252 (event-name 'event))
253 "Define a state machine class called NAME, along with its STATES.
254This macro is (further) syntatic sugar for `define-state-machine',
255`define-state' and `define-enter-state' macros, q.v.
256
257NAME is a symbol. Everything else is specified with a keyword arg.
258
259START and SLEEP are the same as for `define-state-machine'.
260
261STATES is a list, each element having the form (STATE-NAME . STATE-SPEC).
262STATE-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
264a `define-state' or `define-enter-state' call, respectively.
265
266FSM-NAME, STATE-DATA-NAME, CALLBACK-NAME, and EVENT-NAME are symbols,
267used to construct the state functions' arglists."
268 `(progn
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)
274 ,@(cdr it))
275 end
276 if (assq :event spec) collect
277 `(define-state ,name ,state-name
278 (,fsm-name ,state-data-name
279 ,event-name
280 ,callback-name)
281 ,@(cdr it))
282 end)))
283
284(defun fsm-goodbye-cruel-world (name)
285 "Unbind functions related to fsm NAME (a symbol).
286Includes start-NAME, and each fsm-NAME-STATE and fsm-NAME-enter-STATE.
287Functions are `fmakunbound', which will probably give (fatal) pause to
288any state machines using them. Return nil."
289 (interactive "SUnbind function definitions for fsm named: ")
290 (fmakunbound (intern (format "start-%s" name)))
291 (let (ht)
292 (when (hash-table-p (setq ht (get name :fsm-event)))
293 (clrhash ht)
294 (cl-remprop name :fsm-event))
295 (when (hash-table-p (setq ht (get name :fsm-enter)))
296 (clrhash ht)
297 (cl-remprop name :fsm-enter)))
298 nil)
299
300(defun fsm-start-timer (fsm secs)
301 "Send a timeout event to FSM after SECS seconds.
302The timer is canceled if another event occurs before, unless the
303event handler explicitly asks to keep the timer."
304 (fsm-stop-timer fsm)
305 (put fsm
306 :timeout (run-with-timer
307 secs nil
308 #'fsm-send-sync fsm :timeout)))
309
310(defun fsm-stop-timer (fsm)
311 "Stop the timeout timer of FSM."
312 (let ((timer (get fsm :timeout)))
313 (when (timerp timer)
314 (cancel-timer timer)
315 (put fsm :timeout nil))))
316
317(defun fsm-maybe-change-timer (fsm timeout)
318 "Change the timer of FSM according to TIMEOUT."
319 (cond
320 ((numberp timeout)
321 (fsm-start-timer fsm timeout))
322 ((null timeout)
323 (fsm-stop-timer fsm))
324 ;; :keep needs no timer change
325 ))
326
327(defun fsm-send (fsm event &optional callback)
328 "Send EVENT to FSM asynchronously.
329If the state machine generates a response, eventually call
330CALLBACK with the response as only argument."
331 (run-with-timer 0 nil #'fsm-send-sync fsm event callback))
332
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)
340
341 ;; On state change, call enter function and send deferred events
342 ;; again.
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)
348 (condition-case e
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))
353 ((debug error)
354 (fsm-debug-output "%s/%s update didn't work: %S"
355 fsm-name new-state e)))))
356
357 (let ((deferred (nreverse (get fsm :deferred))))
358 (put fsm :deferred nil)
359 (dolist (event deferred)
360 (apply 'fsm-send-sync fsm event))))))
361
362(defun fsm-send-sync (fsm event &optional callback)
363 "Send EVENT to FSM synchronously.
364If the state machine generates a response, eventually call
365CALLBACK with the response as only argument."
366 (save-match-data
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.
380 (cond
381 ((eq result :defer)
382 (let ((deferred (get fsm :deferred)))
383 (put fsm :deferred (cons (list event callback) deferred))))
384 ((null result)
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"
389 fsm-name state
390 (error-message-string (cdr result))))
391 ((and (listp result)
392 (<= 2 (length result))
393 (<= (length result) 3))
394 (cl-destructuring-bind (new-state new-state-data &optional timeout)
395 result
396 (fsm-update fsm new-state new-state-data timeout)))
397 (t
398 (fsm-debug-output "Incorrect return value in %s/%s: %S"
399 fsm-name state
400 result)))))))
401
402(defun fsm-call (fsm event)
403 "Send EVENT to FSM synchronously, and wait for a reply.
404Return the reply. `with-timeout' might be useful."
405 (let (reply)
406 (fsm-send-sync fsm event (lambda (r) (setq reply (list r))))
407 (while (null reply)
408 (fsm-sleep fsm 1))
409 (car reply)))
410
411(defun fsm-make-filter (fsm)
412 "Return a filter function that sends events to FSM.
413Events sent are of the form (:filter PROCESS STRING)."
414 (let ((fsm fsm))
415 (lambda (process string)
416 (fsm-send-sync fsm (list :filter process string)))))
417
418(defun fsm-make-sentinel (fsm)
419 "Return a sentinel function that sends events to FSM.
420Events sent are of the form (:sentinel PROCESS STRING)."
421 (let ((fsm fsm))
422 (lambda (process string)
423 (fsm-send-sync fsm (list :sentinel process string)))))
424
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))
428
429(defun fsm-get-state-data (fsm)
430 "Return the state data of FSM.
431Note the absence of a set function. The fsm should manage its
432state data itself; other code should just send messages to it."
433 (get fsm :state-data))
434
435;;;; ChangeLog:
436
437;; 2016-07-10 Thomas Fitzsimmons <fitzsim@fitzsim.org>
438;;
439;; packages/fsm: Bump version to 0.2.1
440;;
441;; 2016-07-10 Thomas Fitzsimmons <fitzsim@fitzsim.org>
442;;
443;; packages/fsm: Fix compilation error
444;;
445;; * packages/fsm/fsm.el (define-state-machine): Quote default :sleep
446;; lambda (bug#23920).
447;;
448;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
449;;
450;; fsm: Revert some changes suggested by checkdoc
451;;
452;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
453;;
454;; fsm: Bump version to 0.2
455;;
456;; 2015-09-05 Magnus Henoch <magnus.henoch@gmail.com>
457;;
458;; fsm: Change FSMs from plists to uninterned symbols
459;;
460;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
461;;
462;; fsm: Fix copyright
463;;
464;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
465;;
466;; fsm: Add packaging fields
467;;
468;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
469;;
470;; fsm: Fix checkdoc errors
471;;
472;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
473;;
474;; fsm: Add FSM name to some fsm-debug-output messages
475;;
476;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
477;;
478;; fsm: Port to cl-lib
479;;
480;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
481;;
482;; fsm: Use lexical binding
483;;
484;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
485;;
486;; fsm: Fix formatting
487;;
488;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
489;;
490;; fsm: Delete trailing whitespace
491;;
492;; 2015-09-05 Thomas Fitzsimmons <fitzsim@fitzsim.org>
493;;
494;; fsm: Import fsm.el from emacs-jabber
495;;
496;; Import fsm.el from git://git.code.sf.net/p/emacs-jabber/git, commit
497;; 1f858cc4f3cdabcd7380a7d08af273bcdd708c15.
498;;
499
500
501(provide 'fsm)
502
503;;; fsm.el ends here