Copyright © 1998, 2001 by Ian Horswill, see comment within this file.

Discrete actions

Plans and discrete actions

Ian Horswill 4/12/00
 
The plans package provides definitions for behaviors that function as triggerable, discrete actions, as well as explicit sequencing of actions via plans.  Actions has associated with them triggers for initiating them and, optionally, arguments that can be specified at the time the action is triggered.  Actions can be triggered either by normal GRL code or by plans.   Plans form compound actions that can be called and passed arguments by other plans.

The plans package provides GRL definitions for compiling a language of plan expressions into sequences implemented as GRL transducers.  These transducers can them be compiled as a part of a normal GRL program.  Since all plan sequencers compile into transducers, they are all just finite state machines internally.  The good news is that this means they can run in parallel without needing an underlying thread system in the host operating system.   The bad news is that it means none of the sequencers have a stack, and so you can't write recursive or mutually recursive plans.  Fortunately, such things seem relatively infrequent.

NB: Plans are implemented as statically parallel finite-state machines and so are not reentrant.  Attempting to write recursive or mutually recursive plans can lead to strange results, including deadlock and livelock.

Defining primitive actions

Primitive actions are specially flagged behaviors whose activation level is controlled by a register.  Primitive actions may also optionally take arguments.  They are defined with the define-action form:
(define-action (action-name (arg-name  initial-value) ...)
  (initially-running? boolean)
  (trigger-when condition)
  (terminate-when condition)
  motor-vector)
Macro.  Defines a new global variable, action-name, and binds it to a behavior whose action level is controlled by a trigger that is callable by a plan sequencer.  The initial-values, condition, and motor-vector may be arbitrary signal expressions.   The condition and motor-vector expressions are evaluated within the scope of the arguments (Ph.D. talk for saying you can use the arguments in them).  If the initially-running? clause is specified and is true, then the action will be triggered when the program is started.  If the trigger-when clause is specified, then the action can be triggered, either by being explicitly called, or by having the condition specified in the clause be true.  The action will then continue to run until termination, regardless of whether condition continues to be true.  If the terminate-when clause is specified, then the action can be terminated either by being explicitly stopped by some plan, or by having the specified condition be true.
(action (action-name (arg-name  initial-value) ...)
  (initially-running? boolean)
  (trigger-when condition)
  (terminate-when condition)
  motor-vector)
Macro.  Returns a behavior whose action level is controlled by a trigger that is callable by a plan sequencer.  The initial-values, condition, and motor-vector may be arbitrary signal expressions.   The condition and motor-vector expressions are evaluated within the scope of the arguments (Ph.D. talk for saying you can use the arguments in them).   The behavior will automatically drop its activation level when condition is true.

NB: This macro will not properly handle redefinition.  In particular, the sequence:

    (define-signal a (action (arg 0) ...))
    (define-plan (p)
       (a)
       (a))
    (define-signal a (action (arg 0) ...))

will leave the plan p with a pointer to the old version of a.  Use define-action when you want to be able to redefine an action. or talk to Ian about how to write macros that handle redefinition properly.

Defining plans

Plans are compound actions.  They are callable in the same way as normal actions, however, they compile into finite state machines.

Note that primitive actions are behaviors.  They should be combined as usual with whatever other behaviors you wish to support, and then passed to whatever motor controller you use.  Plans, however, are not behaviors in the GRL sense of having an activation level and a motor vector.  Plans do all their word by turning on and off actions and other plans.  As far as the GRL compiler is concerned, an action is of type behavior, meaning it is a group consisting of an activation level and a motor vector.  A plan is a signal whose type is void, i.e. it doesn't have a real value and the compiler simply compiles it for its side effects.

(define-plan (action-name (arg-name   initial-value) ...)
  (initially-running? boolean)
  (trigger-when condition)
  (terminate-when condition)
  (active-when
condition)
  plan-expression ...)
Defines a new global variable, action-name, and binds it to transducer that implements a state-machine to run the plan-expressions in order.  The sequencer is idle until triggered.  Once triggered, it remains active until explicitly stopped or until it finishes the plan.
 
(plan ((arg-name   initial-value) ...)
  (initially-running? boolean)
  (trigger-when condition)
  (terminate-when condition)
  (active-when condition)
  plan-expression ...)
Creates a transducer that implements a state-machine to run the plan-expressions in order.  The sequencer is idle until triggered.  Once triggered, it remains active until explicitly stopped or until it finishes the plan.


NB: as with the action macro, this does not support redefinition properly.  You should use define-plan when you need to bind the resulting plan to a global variable that will be periodically redefined.

Plan expressions

A plan fis specified by a plan expression, which can be any of the following:
 
(scheme scheme-expression) Executes the scheme code verbatim
(action  signal-expression   ...) Triggers action and sets its arguments to the current values of their respective signal-expressions, then waits for the action to terminate.

NB: this operation is unsynchronized.  If action is already running, it will overwrite action's arguments.  To synchronize it, prefix it with (wait-action action).

(start (action  signal-expression   ...)) Triggers action and sets its arguments to the current values of their respective signal-expressions, but does not wait for the action to terminate.

NB: this operation is unsynchronized.  If action is already running, it will overwrite action's arguments.  To synchronize it, prefix it with (wait-action action)

(stop action) Stops the execution of action, if it is currently running.
(wait-action action) Waits for action to terminate.
(sleep milliseconds) Pauses execution of the plan for the specified number of milliseconds.
(wait signal-expression) Waits until signal-expression is true
(set! register signal-expression) Sets register to the current value of signal-expression.
(begin plan-expression  ...) Executes each plan-expression in sequence.
(cobegin plan-expression  ...) Executes each plan-expression in parallel.
(while signal-expression plan-expression ...) A normal while loop: the body is executed repeatedly until signal-expression is false.  The test is only performed at the top of the loop - it is not evaluated continuously.
(if signal-expression
    consequent-plan
    alternative-plan)

(if signal-expression
    consequent-plan)

(when signal-expression
  consequent-plan  ...)

(unless signal-expression
  consequent-plan  ...)

A normal conditional: tests signal-expression, then executes either consequent-plan or alternative-plan (or nothing), depending on the value.
(let ((variable   signal-expression)   ...)
  plan-expression  ...)

(let* ((variable  signal-expression)   ...)
  plan-expression  ...)

Binding construct for local variables, as in LISP.

Caveat: let-bound variables in plans, while they have local scope, are ultimately implemented as global variables with inifinite lifetimes, thus plans with large numbers of local variables will require (somewhat) large amounts of space, even though only a few of those variables are "in use" at any given time.

Rule actions

Primitive actions created with define-action are behaviors; they do their work by returning a motor-vector and an activation level.  Plans do their work through side-effects: they modify registers, call other actions (including plans), and/or they invoke low-level scheme or C code.  Rule actions are a third type of action.  As with plans, they do their work by affecting other components of the system directly, but unlike plans, their operation is controlled by stateless inference rules rather than by a program counter.  For more information on rules, see the rule macro documentation.

(define-rule-action (name (arg-name  initial-value) ...)
 
options  ...
  rules ...)
Creates an action and binds it to name.  When the action is called or otherwise running, the rules are enabled, otherwise it has no effect.  Options can be any of the options allowed in define-action or define-planaction-when, terminate-when, etc.

Generic actions

Generic actions provide a data-directed programming mechanism similar to Common Lisp generic functions.  They are similar to C++ virtual functions except that they allow run-time specialization over multiple arguments (so-called "multimethods").  When a generic action is called, all methods attempt to match themselves to the argument values in parallel.  All matching methods run in parallel until one terminates.  When a method terminates, it clears the shared trigger register, thus halting all methods until the next call.

(define-generic-action (name (arg-name  initial-value) ...))
Defines a generic action with the specified arguments.
 
(define-action-method method-name (generic-action (arg  predicate) ...)
  (active-when condition)
  (terminate-when
condition)
 
motor-vector)
Creates an action (i.e. behavior) that acts as a method for generic-action.  The method is active when the generic has been called, the active-when condition is true, and each predicate is true of its respective arg.
(define-plan-method method-name (generic-action (arg  predicate) ...)
  (active-when condition)
  (terminate-when
condition)
 
plan-expression ...)
Creates a plan that acts as a method for generic-action.  The method is active when the generic has been called, the active-when condition is true, and each predicate is true of its respective arg.
(define-rule-method method-name (generic-action (arg  predicate) ...)
  (active-when condition)
  (terminate-when
condition)
 
plan-expression ...)
Creates a set of rules that act as a method for generic-action.  The method is active when the generic has been called, the active-when condition is true, and each predicate is true of its respective arg.

Plan and action methods can be mixed and matched within a given generic action.

As with signals, the compiler compiles only those methods that are specified in the call to the compiler, or that are inputs to some signal in the call.  This has the advantage that methods can be defined in a library and then configured as desired by the user simply by adjusting what methods are given in the compiler call.  It has the disadvantage that the user has to remember to include all the desired methods.

There is also a mechanism for defining default methods that run if and only if not other method matches: 

(define-default-action-method method-name (generic-action arg ...)
  (terminate-when condition)
 
motor-vector)
Creates an action (i.e. behavior) that acts as a method for generic-action.  The method is active when the generic has been called, the active-when condition is true, and no other method is active.
(define-default-plan-method method-name (generic-action arg  ...)
  (terminate-when condition)
 
plan-expression ...)
Creates a plan that acts as a method for generic-action.  The method is active when the generic has been called, the active-when condition is true, and no other method is active.
(define-default-rule-method method-name (generic-action arg  ...)
  (terminate-when condition)
 
rule ...)
Creates a set of rules that act as a method for generic-action.  The method is active when the generic has been called, the active-when condition is true, and no other method is active.
 
These methods have no argument predicates because their activation signals are computed automatically by the compiler.

Generic actions are effectively implemented as vestigial actions with no motor vectors.  That is, they have trigger and argument registers but no actual code to execute.  Methods are implemented as normal actions or plans that don't have their own trigger and argument registers, but rather use the trigger and argument registers of the generic action.

Plan macros

There is also a macro facility available for cases where a procedural abstraction is desired but it cannot be phrased as a separate plan.  The usual reason for this is that multiple concurrent instances of the procedure are needed.  GRL plans aren't reentrant, so concurrent calls interfere with one another.

(plan-macro (args ...) body ...)
Creates a macro that can be called from within a plan.
(define-plan-macro (name  args ...) body ...)
Creates a macro that can be called from within a plan and binds it to name.

Built-in plan macros

(return)
Terminates execution of the plan.
(exit value)
Forces the GRL program to exit completely.  If the underlying run-time environment supports returning values on exit, then value will be returned.
(assert! accumulator)
Assigns #t to the specified signal, which must be a boolean-valued accumulator over or.
Example: (assert! (goal t)), will activate the goal modality for a binary signal t.
(assert! accumulator  roleset)
Sets the bits in roleset in accumulator, which must be a integer-valued accumulator over or.
Example: (assert! (goal near-object?) patient), will make (near-object? patient) a goal..
(retract! accumulator)
Assigns #f to the specified signal, which must be a boolean-valued accumulator over or.
Example: (retract! (goal t)), will deactive the goal modality for a binary signal t.
Caveat: assert! and retract! both work by creating a register to hold the value being asserted and then setting the register to drive the accumulator.  So if some other signal drives the accumulator directly, retract will not be able to override it.
(retract! accumulator  roleset)
Resets the bits in roleset in accumulator, which must be a integer-valued accumulator over or.
Example: (retract! (goal near-object?) patient), will cancel the goal (near-object? patient).
Caveat: assert! and retract! both work by creating a register to hold the value being asserted and then setting the register to drive the accumulator.  So if some other signal drives the accumulator directly, retract will not be able to override it.

Miscellaneous

It is very unlikely that you actually want to use this stuff.  There's generally a cleaner way to achieve the same ends.

(call-when condition  action  args  ....)
Signal procedure.  Calls action with args whenever condition is true.
(call  action  args  ....)
Signal procedure.  An accumulator that calls action with args whenever it is driven with #t.  Probably useful only in rulesets and the on-activation/on-termination clauses of frames.
(active? action)
Signal procedureAction can be a primitive action or a plan.   Returns true or if the action is running.
(done? action)
Signal procedureAction can be a primitive action or a plan.   Returns true or if the action is has terminated (or has never been started).

Internals

Ultimately, a call to a plan or discrete action is compiled into state machine state with the following object code:
State 0:
(set! action-trigger #t)
(set! action-arg1 value1)
...
(set! action-argn valuen)
(set! my-state 1)
State 1:
(when (not plan-trigger)
  (set! my-state 2))
This code would be situated inside of a case statement that is called once per cycle of the GRL control loop and that branches based on the value of my-state.

Signal properties

Information about actions (plan and primitive actions) is stored in the following properties of the property list.  You can make other kinds of objects act like actions (i.e. be callable) by filling these in appropriately.
action?
True if the signal is an action, that is, if it is callable by the normal action calling sequence.
action-trigger
The action's trigger (a signal which is a shared register).
action-args
The action's a list of the registers that hold the action's arguments, in order.

Low-level procedures

(action? signal)
Procedure.  Returns #t if the signal is an action.
(assert-action! signal)
Procedure.  Throws an error is signal is not an action.   Otherwise returns without doing anything.
(action-trigger action)
Procedure.  Returns the trigger register of action.
(action-args action)
Procedure.  Returns the argument registers of action.