The interpreter: figuring out what to do next

Previous: Making decisions: wftk_decide ] [ Top:  ] [ Next: wftk core library ]

The interpreter consists of two functions, queue_procdef and process_procdef. Both work from the queue which is part of the state of the process. The queue_procdef function takes a procdef or piece of one and adds it to the queue. Simple enough. The process_procdef function, then, does the bulk of the work. It runs through the queue, looking at each queued item in turn. If the "block" attribute of an item is "yes", then it's skipped; otherwise it is consumed. Various actions, of course, can then queue other items. The function returns only when the queue is empty (at which point the process is complete) or everything in the queue is blocked, meaning that there are only tasks waiting on further input.

Let's look at queue_procdef first.
 
int unique_id (XML * datasheet, XML * state) {
   int idcount;

   if (!state) {
      state = xml_loc (datasheet, ".state");
      if (!state) {
         state = xml_create ("state");
         xml_append (datasheet, state);
      }
   }

   idcount = atoi (xml_attrval (state, "idcount"));
   idcount++;
   xml_setnum (state, "idcount", idcount);

   return idcount;
}

XML * queue_procdef (void * session, XML * datasheet, XML * state, XML * queue, XML * action, const char * where)
{
   XML * item;
   int  idcount;

   if (action == NULL) return NULL;

   idcount = unique_id (datasheet, state);

   item = xml_create("item");
   xml_setnum (item, "id", idcount);
   xml_set (item, "type", xml_name(action));
   xml_set_nodup (item, "loc", xml_getlocbuf (action));
   xml_set (item, "where", where);
   xml_set (item, "oncomplete", xml_attrval (action, "oncomplete"));
   xml_set (item, "fortask", xml_attrval (action, "fortask"));
   xml_append (queue, item);
   return (item);
}
That's pretty straightforward, right? It assumes that the state and queue have already been found or created, but it can handle a state which doesn't have an idcount yet. The idcount value is used to ensure that item identifiers in the queue are unique, by the simple expedient of counting them.

OK. So what does it take to do the interpretation itself? Not too much, actually. Let's look at the overall structure of process_procdef, then talk about why it's the way it is, and then we'll fill in the various actions afterwards.
 
void process_procdef(void * session, XML * datasheet, XML * state, XML * queue, XML * procdef)
{
   XML * item;
   XML * def;
   XML * holder;
   XML * mark;
   XML * task;
   XML * data;
   XML * next;
   char * value;
   const char * type;
   int count;
   int keep;
   WFTK_ADAPTORLIST * adlist;

   item = xml_firstelem (queue);

   while (item != NULL) {
      if (!strcmp("yes", xml_attrval(item, "block"))) {
         item = xml_nextelem(item);
         continue;
      }
      if (!strcmp (xml_attrval (item, "where"), "datasheet")) {
         def = xml_loc (datasheet, xml_attrval(item, "loc"));
      } else {
         def = xml_loc (procdef, xml_attrval(item, "loc"));
      }
      type = xml_attrval (item, "type");

      keep = 0;
      next = NULL;
             if (!strcmp (type, "parallel")) {
         See Handling parallel
      } else if (!strcmp (type, "task")) {
         See Handling task
      } else if (!strcmp (type, "action")) {
         See Handling action
      } else if (!strcmp (type, "data")) {
         See Handling data
      } else if (!strcmp (type, "situation")) {
         See Handling situation
      } else if (!strcmp (type, "decide")) {
         See Handling decide
      } else if (!strcmp (type, "alert")) {
         See Handling alerts
      } else if (!strcmp (type, "start")) {
         See start: Starting subprocesses
      } else { /* Treat it as a sequence. */
         See Handling sequence
      }

      if (keep) {
         xml_set (item, "block", "yes");
         item = xml_nextelem(item);
      } else {
         if (*xml_attrval (item, "oncomplete")) {
            wftk_status_set (session, datasheet, xml_attrval (item, "oncomplete"));
         }
         if (*xml_attrval (item, "parent")) {
            next = xml_locf (queue, "queue.item[%s]", xml_attrval (item, "parent"));
            xml_delete (item);
            item = next;
            xml_set (item, "block", "no");
         } else {
            if (*xml_attrval (item, "fortask")) {
               holder = xml_locf (datasheet, ".state.queue.item[%s]", xml_attrval (item, "fortask"));
               if (holder) { /* A workflow task; we'll just handle that case right in the interpreter. */
                  xml_delete (item);
                  item = holder;
               } else { /* Maybe it's an ad-hoc task or something; wftk_task_complete can handle it. */
                  holder = xml_create ("task");
                  xml_set (holder, "id", xml_attrval (item, "fortask"));
                  xml_delete (item);
                  item = NULL;
                  xml_set (holder, "dsrep", xml_attrval (datasheet, "repository"));
                  xml_set (holder, "process", xml_attrval (datasheet, "id"));
                  wftk_task_complete (session, holder);
                  xml_delete (holder);
               }
            } else {
               xml_delete (item);
               item = NULL;
            }
         }
      }
   }
}
As you can see, the whole thing is the loop which walks along the queue. If an item is blocked, we skip it, otherwise we process it. The details of processing are below, and remember that processing might entail the queuing of new items. Once the item has been processed, it may either be blocked, in which case we block it, or it's finished and we delete it. If it's finished, control returns to its parent, if it has one, and things go on. If the parent decides to block, then the queue continues. Pretty neat, actually. The "parent" here is not the parent node of the procdef action, but rather the parent item of the completed item in the queue. Its identifier is thus a number created by queue_procdef.

Definition of _state structure
In the prototype, state was kept as global variables. If the wftk core engine were always called as a simple command-line program, this would work out fine, I suppose, but if we want to build it into things like linakable libraries, we have to worry about contention. So I've split out the global state into a state structure which is to be passed around internally to keep track of where we are.
 
struct _state {
   XML * datasheet;
   XML * state;
   XML * queue;
   XML * procdef;
};


Handling sequence
So let's look at the individual action handlers for the above, starting with the sequence handler. The sequence handler takes care of the sequence tag and also the contents of the outer workflow tag (which are executed sequentially). If the "cur" attribute is not yet set, then this is the first time we've encountered this sequence, and we queue up the first child of the sequence (and note its location with "cur", of course). Otherwise, we find the child located by "cur", find its next sibling, and queue that up.

If something gets queued, then we block. Remember, we signify blocking by setting keep to 1.
 
if (!strcmp ("", xml_attrval (item, "cur"))) {
   next = xml_firstelem (def);
} else {
   if (!strcmp (xml_attrval (item, "where"), "datasheet")) {
      next = xml_loc (datasheet, xml_attrval (item, "cur"));
   } else {
      next = xml_loc (procdef, xml_attrval (item, "cur"));
   }
   next = xml_nextelem (next);
}

if (next) {
   xml_set (queue_procdef (session, datasheet, state, queue, next, xml_attrval (item, "where")), "parent", xml_attrval (item, "id"));
   xml_set_nodup (item, "cur", xml_getlocbuf (next));
   keep = 1;
}


Handling parallel
The parallel item queues up all its children, then blocks. When a child completes, it counts the number of children complete; when its counter decrements to zero, it completes.
 
if (!strcmp ("", xml_attrval (item, "remaining"))) {
   count = 0;
   next = xml_firstelem (def);
   while (next != NULL) {
      count ++;
      xml_set (queue_procdef (session, datasheet, state, queue, next, xml_attrval (item, "where")), "parent", xml_attrval (item, "id"));
      next = xml_nextelem (next);
   }
} else {
   count = xml_attrvalnum (item, "remaining");
   count--;
}
xml_setnum (item, "remaining", count);
if (count > 0) keep = 1;


Handling task
Doing a task is nothing more than setting up task data and telling the task manager that the task has been activated. We'll take as the task ID the process ID plus our internal task ID; this will make things easier to handle in the task manager, as it means our task IDs will always be unique (well, assuming the task manager always gives us unique process IDs.)

If the task defines data, then we create a placeholder in the datasheet when the task is activated. This is to allow things like option lists to get their values for display, and so that the value will show up in the value list (as a blank, of course).
 
if (strcmp (xml_attrval (item, "block"), "resume")) {
   /* Notify task indices of the new active task. */
   task = xml_create ("task");
   xml_set (task, "id", xml_attrval (item, "id"));
   xml_set_nodup (task, "label", wftk_value_interpreta (session, datasheet, xml_attrval (def, "label")));
   xml_set (task, "role", xml_attrval (def, "role"));
   xml_set (task, "user", xml_attrval (def, "user"));
   if (!*xml_attrval (task, "user") && *xml_attrval (task, "role")) {
      xml_set (task, "user", wftk_role_user (session, datasheet, xml_attrval (task, "role")));
   }
   xml_set (task, "process", xml_attrval (datasheet, "id"));

   mark = xml_firstelem (def);
   while (mark) {
      if (xml_is (mark, "data")) {
         if (!wftk_value_find (session, datasheet, xml_attrval (mark, "id"))) {
            xml_append (datasheet, xml_copy (mark));
         }
      }
      mark = xml_nextelem (mark);
   }

   adlist = wftk_get_adaptorlist (session, TASKINDEX);
   wftk_call_adaptorlist (adlist, "tasknew", task);
   wftk_free_adaptorlist (session, adlist);
   xml_free (task);

   keep = 1; /* This blocks the current item, so that the active task will be available for retrieval. */
}


Handling action
Taking action requires decorating the action specifier with the current datasheet, procdef, and userid (from the session). Some attributes will also be interpreted at some point, but as that's not yet entirely clear, I'll just defer it for the time being.
 
/* Notify task indices of the new active task. */
if (!strcmp (xml_attrval (item, "where"), "datasheet")) {
   mark = xml_loc (datasheet, xml_attrval (item, "loc"));
} else {
   mark = xml_loc (procdef, xml_attrval (item, "loc"));
}
if (mark) {
   holder = xml_copy (mark);
   if (wftk_session_getuser (session)) {
      xml_set (holder, "by", xml_attrval (wftk_session_getuser(session), "id"));
   }
   xml_set (holder, "dsrep", xml_attrval (datasheet, "dsrep"));
   xml_set (holder, "process", xml_attrval (datasheet, "id"));
   xml_set (holder, "pdrep", xml_attrval (procdef, "pdrep"));
   xml_set (holder, "procdef", xml_attrval (procdef, "id"));

   wftk_action (session, holder);
}


Handling data
The data element is really just an assignment, nothing more. Along with decisions, I think that simple assignment will be enough. I'll also write an eval data "source" which evaluates expressions, and that will give us plenty of power. Scripts, of course, are actions.
 
if (!strcmp (xml_attrval (item, "where"), "datasheet")) {
   mark = xml_loc (datasheet, xml_attrval (item, "loc"));
} else {
   mark = xml_loc (procdef, xml_attrval (item, "loc"));
}
if (mark) {
   value = wftk_value_interpreta (session, datasheet, xml_attrval (mark, "value"));
   wftk_value_set (session, datasheet, xml_attrval (mark, "id"), value);
   free (value);
}


Handling situation
TODO: To be implemented later. Oy.

Handling decide
Decisions use the wftk_decide function, documented rather completely
here.
 
if (!strcmp ("", xml_attrval (item, "cur"))) {
   if (!strcmp (xml_attrval (item, "where"), "datasheet")) {
      mark = xml_loc (datasheet, xml_attrval (item, "loc"));
   } else {
      mark = xml_loc (procdef, xml_attrval (item, "loc"));
   }
   if (mark) {
      holder = wftk_decide (session, datasheet, mark);
      if (*xml_attrval (holder, "loc")) {
         if (!strcmp (xml_attrval (item, "where"), "datasheet")) {
            next = xml_loc (datasheet, xml_attrval (holder, "loc"));
         } else {
            next = xml_loc (procdef, xml_attrval (holder, "loc"));
         }
      }
      xml_free (holder);
   }
}
if (next) {
   xml_set (queue_procdef (session, datasheet, state, queue, next, xml_attrval (item, "where")), "parent", xml_attrval (item, "parent"));
   keep = 1;
}


Handling alerts
Alerts use the function wftk_notify, which is basically a wrapper for notification adaptors. The nice thing about this is that it allows us to expose the function itself, which makes it a convenient little addition to the library.
 
wftk_notify (session, datasheet, def);


start: Starting subprocesses
TODO: To be implemented later. Basically it creates a new simple task, creates a new process, and calls wftk_process_attach to attach the subprocess.
Previous: Making decisions: wftk_decide ] [ Top:  ] [ Next: wftk core library ]


This code and documentation are released under the terms of the GNU license. They are additionally copyright (c) 2000, Vivtek. All rights reserved except those explicitly granted under the terms of the GNU license.