Version 0 of Finite state machines

Updated 2002-04-23 19:48:16

Arjen Markus They are very powerful programming devices, but most languages are unsuitable to actually define them: (finite) state machines. Everyday examples: parsers in Yacc/Lex, regular expressions, protocol handlers and so on.

Below is a small script, inspired by a paper on Forth, that shows that Tcl can retain the table-like appearance that one so dearly loves, when specifying a state machine. (Okay, it does little or no error checking, it defines only deterministic finite state machines, and the example is overly simple, but still, have a look).


   # statemachine.tcl --
   #
   #    Script to implement a finite state machine
   #
   # Version information:
   #    version 0.1: initial implementation, april 2002


   namespace eval ::FiniteState {
      variable statemachine

      namespace export defineMachine evalMachine resetMachine
   }

   # defineMachine --
   #    Define a finite state machine and its transitions
   #
   # Arguments:
   #    machine      Name of the machine (a variable)
   #    states       List of states
   #
   # Result:
   #    None
   #
   # Side effects:
   #    The variable "machine" is filled with the definition
   #
   # Notes:
   #    The list of states can only contain the commands initialState
   #    and state. No others are allowed (no check though)
   #
   #    For instance:
   #    defineMachine aMachine {
   #       initialState 1
   #       state 1 {
   #          "A" 2 {puts "To state 2"}
   #          "B" 3 {puts "To state 3"}
   #       }
   #       state 2 {
   #          "C" 1 {puts "To state 1"}
   #          "D" 3 {puts "To state 3"}
   #       }
   #       state 3 {
   #          "E" 3 {exit}
   #       }
   #    }
   #
   #
   proc ::FiniteState::defineMachine { machine states } {
      upvar $machine machinedef

      set machinedef {}
      set first_name {}
      set maxarg     [llength $states]
      for { set idx 0 } { $idx < $maxarg } { incr idx } {
         set arg [lindex $states $idx]
         switch -- $arg {
         "state" {
             set statename   [lindex $states [incr idx]]
             set transitions [lindex $states [incr idx]]
             lappend machinedef $statename $transitions
         }
         "initialState" {
             set first_state [lindex $states [incr idx]]
         }
         default {
         }
         }
      }

      #
      # First three items are reserved: the initial state and the current
      # state. By storing them in the same list we can pass the
      # information around in any way needed.
      #
      set machinedef [concat $first_state $first_state $machinedef]
   }

   # evalMachine --
   #    Evaluate the input and go to the next state
   #
   # Arguments:
   #    machine      Name of the machine (a variable)
   #    input        The input to which to react
   #
   # Result:
   #    None
   #
   # Side effects:
   #    The machine's state is changed and the action belonging to the
   #    transition is executed.
   #
   proc ::FiniteState::evalMachine { machine input } {
      upvar $machine machinedef

      set current_state [lindex  $machinedef 1]
      #
      # Look up the state's transitions
      #
      set states      [lrange $machinedef 2 end]
      set idx         [lsearch $states $current_state]
      set transitions [lindex  $states [incr idx]]

      set found 0
      foreach {pattern newstate action} $transitions {
         if { $pattern == $input } {
            uplevel $action
            set found 1
            break
         }
      }
      if { $found } {
         set machinedef [lreplace $machinedef 1 1 $newstate]
      } else {
         #error "Input ($input) not found for state $current_state"
         # Or rather: ignore
      }
   }

   # resetMachine --
   #    Reset the machine's state
   #
   # Arguments:
   #    machine      Name of the machine (a variable)
   #
   # Result:
   #    None
   #
   # Side effects:
   #    The machine's state is changed to the initial state.
   #
   proc ::FiniteState::resetMachine { machine } {
      upvar $machine machinedef

      set initial_state [lindex  $machinedef 0]
      set machinedef [lreplace $machinedef 1 1 $current_state]
   }

   #
   # Define a simple machine to test the code:
   # A furnace that needs to keep the same temperature, so the heating
   # may be on or off
   #
   namespace import ::FiniteState::*

   defineMachine heater {
      initialState off
      state off {
         "too_cold" on  { set heating $heat_capacity}
      }
      state on {
         "too_hot"  off { set heating 0 }
      }
   }

   set time            0.0
   set dt              0.1
   set temp_amb       20.0
   set temp           $temp_amb
   set temp_ideal    200.0
   set exch            0.3
   set heating         0.0
   set heat_capacity 500.0

   while { $time < 10.0 } {
      evalMachine heater \
         [expr {$temp<=$temp_ideal? "too_cold" : "too_hot" }]

      set time [expr {$time+$dt}]
      set temp [expr {$temp+$dt*($exch*($temp_amb-$temp)+$heating)}]
      puts "$time $temp $heating [lindex $heater 1]"
   }

Trying to play Forth Arts and crafts of Tcl/Tk