fsm.lisp 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. ;;;; fsm.lisp
  2. #|
  3. An FSM can be conveniently represented by means of a diagram. Until such a
  4. diagram is included as documentation, we'll have to content ourselves by its
  5. faithful representation by S-expressions, see e. g. the backgammon-machine
  6. definition in event.lisp
  7. We use a fairly straightforward implementation of an FSM as a CASE over the
  8. states, and a COND for each state choosing the arc. By default the event is
  9. ignored, i. e. we remain in the same state and do nothing.
  10. The choice of an arc is determined by the control state, the event, and the
  11. game state. However, the machine only needs the value of a few boolean
  12. variables to make the choice. Every COND-form choosing an arc is evaluated in
  13. a lexical scope where the variables are defined. This scope is introduced by
  14. the state-bindings function. As the set of variables and the rules for their
  15. computation varies from state to state (and from machine to machine, of
  16. course), state-bindings is a generic function dispatching on the names of the
  17. machine and the state. Perhaps a better dispatch can be thought of (when I
  18. used to have two machines, some code was duplicated), but anyway, it's a cool
  19. opportunity to use generic functions for code generation.
  20. The defmachine macro transform the description of a state machine into an event
  21. handler, whose arguments are the event and the controlled objects. Instead of
  22. inlining the actions into the handler, we look them up at run time in the table
  23. *machines*. This table assigns to each machine an alist of arcs. The keys of
  24. the alist are S-expressions representing the start state of the arc and its
  25. condition, the values are correspondent actions. Actually, this is a bit more
  26. cumbersome to implement than inlining and in theory, the runtime lookup is less
  27. efficient. However, the lookup is hardly the bottleneck. On the other hand,
  28. `interpreting' the machine allows to modify rules on the fly, which is
  29. convenient for small fixes, and we may be saving memory by not inlining the
  30. actions.
  31. Perhaps STATE-BINDINGS should be moved to run time as well?
  32. |#
  33. (in-package #:fsm)
  34. (eval-when (:compile-toplevel :load-toplevel :execute)
  35. (defgeneric state-bindings (machine state machine-arguments))
  36. ;; machine, state: symbols
  37. ;; condition: S-expression
  38. ;; args: the argumenst of the machine
  39. ;; A part of a COND form is generated: if condition holds, look up the action
  40. ;; associated with the symbolic form of the condition and apply it to args.
  41. (defun cond-clause (machine state condition args)
  42. `(,condition (funcall (second (assoc '(,state ,condition) (gethash ',machine *machines*) :test #'equal))
  43. ,@args)))
  44. ;; Collect the COND clauses into a COND form. In each branch, setf the
  45. ;; state-v variable to the value of the next state.
  46. ;; arcs: list of plists
  47. (defun state-cond (state-v machine state arcs args)
  48. `(cond ,@(let ((clauses (list `(t ,state-v))))
  49. (dolist (arc (reverse arcs) clauses)
  50. (push (append (cond-clause machine state (getf arc :when) args)
  51. (list `(setf ,state-v ',(getf arc :to))))
  52. clauses)))))
  53. ;; The CASE form over the states. The car of each rule is its type. We are
  54. ;; only interested in :state rules. For any such rule, (second rule) is its
  55. ;; name, and (cddr rule) is the list of arcs, each arc being represented by :arc
  56. ;; consed onto a plist.
  57. (defun states-case (state-v machine rules args)
  58. `(case ,state-v ,@(loop for rule in rules
  59. when (eql (first rule) :state)
  60. collect `(,(second rule) ,(funcall (state-bindings machine (second rule) args)
  61. (state-cond state-v
  62. machine
  63. (second rule)
  64. (loop for (head . tail) in (cddr rule)
  65. when (eql head :arc)
  66. collect tail)
  67. args))))))
  68. ;; When defining the handler, first register all the arcs.
  69. (defun arc-register-form (name arguments rules)
  70. `(progn
  71. ,@(loop for rule in rules
  72. when (eql (first rule) :state)
  73. append (loop for (head . tail) in (cddr rule)
  74. when (eql head :arc)
  75. collect `(register-arc ',name
  76. ',(second rule)
  77. ',(getf tail :when)
  78. (lambda (,@arguments)
  79. (declare (ignorable ,@arguments))
  80. ,(getf tail :action)))))))
  81. (defun handler-definition-form (name arguments rules state-v)
  82. `(defun ,name ()
  83. (let ((,state-v ',(second (assoc :initial-state rules))))
  84. (lambda (,@arguments)
  85. ,(states-case state-v name rules arguments)))))
  86. )
  87. (defvar *machines* (make-hash-table :test 'eq))
  88. (defun undefine-machine (machine)
  89. (remhash machine *machines*))
  90. (defun register-arc (machine state condition action)
  91. (push (list (list state condition)
  92. action)
  93. (gethash machine *machines*)))
  94. (defun call-arc (machine state condition &rest args)
  95. (let ((action (second (assoc (list state condition)
  96. (gethash machine *machines*)
  97. :test #'equal))))
  98. (when action
  99. (apply action args))))
  100. (defmacro defmachine (name (&rest arguments) &body rules)
  101. (let ((state-v (gensym "STATE-")))
  102. `(progn
  103. (undefine-machine ',name)
  104. ,(arc-register-form name arguments rules)
  105. ,(handler-definition-form name arguments rules state-v))))