The Task Description Language Technical Report.

1 Introduction

The C++ programming language is a very powerful language for writing software. However, it is designed to perform tasks sequentially. While a programmer can make use of threads or inter-process communication to implement concurrency, such programs tend to be overly complex, difficult to modify, and therefore difficult to maintain. With the further drawback that the programmer must spend an inordinate amount of time and effort dealing with the high programmatic impact of these forms of concurrency.

For these reasons, TCM, the Task Control Management System, and its predecessor, TCA, the Task Control Architecture, were developed. TCM permits users to create Tasks, which can then be executed concurrently or sequentially within a wide range of constraints. Because TCM implements concurrency for the programmer, the programmatic impact is substantially reduced. With the end result that programs are easier to write, understand, and maintain.

Unfortunately, while the programmatic impact of concurrency is substantially reduced by using TCM, it is still considerable. And it is repetitive by nature. For example: The creation of data structures for passing arguments, the wrapping and unwrapping of arguments in these data structures, the utilization of pointers to functions, or the proper invocations of constraints with their appropriate data structures.

But such repetitive work is an ideal target for computer automation. Which has led us to develop TDL, the Task Description Language. TDL extends the C++ syntax to introduce a new procedure-equivalent called a Task, as well as mechanisms for creating and constraining tasks. TDLC, the Task Description Language Compiler, compiles TDL code into C++ code. This C++ code can then be compiled with any C++ compiler, linked with the TCM library, and run.

To clarify one point up front: TCA, TCM, TDL, and TDLC facilitate the writing of concurrent software. That is to say, they deal with the programmatical aspects of having more than one thread of execution running at the same time. They do not perform magic. While this software has facilities to permit programmers to prevent race conditions, livelock and so forth, it does not do so automatically for the programmer. Rather, TDL (through TCM) provides a mechanism for constraining concurrent Tasks. It is the responsibility of the programmer to properly utilize the constraining options to prevent race conditions, livelock, and so forth.



2 History

In the beginning, most programmers start out writing single massive monolithic programs, with few or no subroutines. Fortunately education, and in some cases evolution, rapidly encourages programmers towards functional decomposition. With functional decomposition, a programmer does not need to write identical code in multiple locations. Instead, that code can occur once, in a single subroutine, which is then invoked from multiple locations. This permits the programmer to manage significantly more complex pieces of software.

If one considers each subroutine, or function, to be a node on a graph, one can then construct a function call graph for any such program. A node in a function call graph has one child for every subroutine, or function, that that node's corresponding function actually invokes. Which implies that a function call graph can differ between successive invocations of a program.

The function call graph has another feature. It is traversed in a depth-first manner. And the code corresponding to each node is executed sequentially. But consider the case where we want some of these nodes to be executed concurrently. Here functional decomposition begins to break down.

Either one uses specialized hardware, such as vectorized processors or iWarp, with their correspondingly specialized software languages. At the price of shifting the programming paradigm away from functional decomposition. Or one makes use of operating system or language specific features to create new Threads or Tasks. At the price of breaking the function call graph, and forming new root nodes for each concurrent subroutine.

TDL was invented to change this. In TDL, we have the concept of Tasks. Tasks are procedures. They are similar to functions, but they do not return a value. Tasks coexist along with functions. Tasks can be invoked as functions, or from functions. And functions can be invoked from Tasks.

But while functions utilize a function call graph, Tasks have a corresponding, yet independent, Task call graph that is commonly referred to as the Task-Tree. And while running functions concurrently requires significant effort on the part of the programmer, running Tasks concurrently is trivial. For example:

WITH ( PARALLEL )

{

SPAWN A();

SPAWN B();

}



And, just as easily, a programmer could run these Tasks sequentially. For example:

WITH ( SERIAL )

{

SPAWN A();

SPAWN B();

}



But, regardless of whether Tasks are concurrent or sequential, they are never removed from the Task-Tree. They never form a new root node. And they never form an independent Task call graph (Task-Tree). Instead, all the Tasks form a single Task call graph (Task-Tree). Which permits TDL to offer concurrent functional decomposition. And permits the programmer to construct complex software, subsections of which are allowed to operate concurrently. For example, as illustrated in Figure 1:



WITH ( SERIAL )

{

SPAWN B();



WITH ( PARALLEL )

{



WITH ( SERIAL )

{

SPAWN C();

SPAWN D();



WITH ( PARALLEL )

{

SPAWN E();

SPAWN F();

}

}



WITH ( SERIAL )

{

SPAWN G();

SPAWN H();

}

}



SPAWN I();

}



This functionality is directly applicable to our targeted realm of autonomous control. For example, in robotics, it is frequently desirable to execute the currently planned motion while planning for the next subsequent motion. This permits the robot to perform near-continuous movement, rather than stopping to plan after every motion.

However, that actual planning for motion can become quite complex. Leading programmers to utilize elaborate decision trees, recursion, and other programming strategies to derive the planned motion. Naturally, passing the planning results back up through multiple levels of nesting and recursion would create a substantial programmatical overhead. And yet, we still wish to be able to constrain when the implementation of the plan is permitted to occur.

But TDL has the Task-Tree, the Task call graph. And since the parent-child Task relationships are preserved, we should be able to utilize this knowledge to constrain the descendants of a Task. Allowing the bottom-most nested planning Task to create a Task that implements the plan, and interacts with the real-world environment.

TDL accomplishes this through two specific types of Tasks. Goal Tasks are assumed to be involved in the planning operations. Command Tasks are assumed to be involved in the real-world implementation of the plan. Goals can form a complex decision tree, deciding what plan to follow. And Commands can form the leaves, implementing the plan that has been decided upon by the Goal Tasks.

Every Task is then considered to have both an Expansion phase, during which its Goal descendants are running, and a Execution phase, during which its Command descendants are running. These phases can overlap. But by constraining when the Expansion or Execution phases are permitted to begin, one can achieve the desired control through the TDL language itself.



3 Getting Started

TDL extends the C++ programming language. Which is to say that you can freely mix C++ code and TDL extensions in the same file. The TDL extensions permit programmers to spawn Tasks inside C++ functions. (Such a Task is added to the root level of the Task-Tree.) And they permit programmers to declare Tasks. In its simplest form, a Task declaration looks very similar to a function declaration. With the exception that the return value is replaced by the Task's Type (Goal or Command). For example:

GOAL foo ( int I, double j )

{

printf ( "foo\n" );

}



Declared Tasks can be invoked directly, as functions. For example:

GOAL bar()

{

foo ( 2, 3.4 );

}



When a Task [foo] is invoked as a function, TDL mimics the behavior of a normal function call. Further processing of the calling Task [bar] is blocked until function-invoked Task [foo], and all of its descendants, have completed running. The Task [foo] is still added to the Task-Tree, as a child of the calling Task [bar]. But because the invocation mechanism blocks until the completion of the Task [foo], subsequent Tasks [in bar] can not run concurrently with the function-invoked Task [foo].

Given the desirability of running Tasks concurrently, TDL supports a secondary means of invoking Tasks through the keyword SPAWN. SPAWN'ed Tasks are run concurrently by default. For example:

GOAL bar()

{

SPAWN f1();

SPAWN f2();

}



This would run Task's f1 and f2 concurrently. If one prefers f2 to be run after f1, and all of f1's descendent Tasks, have finished, one can make use of the optional WITH clause of the SPAWN statement. For example:

GOAL bar()

{

SPAWN f1();

SPAWN f2() WITH SERIAL f1;

}



The WITH clause allows one to specify a comma separated list of one or more constraints for a single specific SPAWN operation. SERIAL is one such constraint. If a specific target [f1] is not specified for the SERIAL constraint, it defaults to the previous SPAWN (or WITH statement) operation. So the above example would be equivalent to:

GOAL bar()

{

SPAWN f1();

SPAWN f2() WITH SERIAL;

}



If we wished to apply the same constraint to multiple SPAWN operations, we could repeat the WITH clause for each SPAWN operation. For example:

GOAL bar()

{

SPAWN f1();

SPAWN f2() WITH SERIAL;

SPAWN f3() WITH SERIAL;

SPAWN f4() WITH SERIAL;

}



But this is inefficient. So instead, we introduce the WITH statement. The WITH statement applies a set of constraints to all the SPAWN operations contained in the WITH statement's body. So the above example would be equivalent to:

GOAL bar()

{

WITH ( SERIAL )

{

SPAWN f1();

SPAWN f2();

SPAWN f3();

SPAWN f4();

}

}



The WITH statement also allows one to utilize the PARALLEL constraint, since the PARALLEL constraint is meaningless unless applied to two or more SPAWN operations simultaneously. For example:

GOAL bar()

{

WITH ( SERIAL )

{

SPAWN f1();



WITH ( PARALLEL )

{

SPAWN f2();

SPAWN f3();

}



SPAWN f4();

}

}



Which results in f2 and f3 running concurrently after f1, and all of f1's descendants, have completed. And f4 running after both f2 and f3, and all of their descendants, have completed.

One should note that SERIAL does not imply that the calling task [bar] is blocked. In fact, SERIAL does not imply any relationship with the calling task [bar] at all. Rather, it merely implies a relationship between the two subtasks. Consider:

Goal g1()

{

printf ( "1" );

}



Goal g2()

{

printf ( "2" );

}



Goal b1()

{

printf ( "A" );

g1();

printf ( "B" );

g2();

printf ( "C" );

}



Goal b2()

{

printf ( "A" );

SPAWN g1();

printf ( "B" );

SPAWN g2() with SERIAL g1;

printf ( "C" );

}



In both cases we will always have "1" printing before "2", and "A" printing before "B" printing before "C". But only in Task b1 are we guaranteed to get the sequence "A1B2C". For Task b2, this guarantee can not be made. In Task b2, g1 could be started after the printf ( "C" ). The only guarantee in Task b2 is that Task g2 will not be started until after Task g1 has completed. The relationship between when Task g1 and g2 start with respect to Task b2 is completely undefined.

As this behavior is not always desirable, TDL offers the WAIT constraint. The WAIT constraint implies that the SPAWN'ing operation should block until the WAIT'ed Task, and all of that Task's descendants, have completed. Consider:

Goal b3()

{

printf ( "A" );

SPAWN g1();

printf ( "B" );

SPAWN g2() with SERIAL g1, WAIT;

printf ( "C" );

}



In Task b3 we are now guaranteed that the invocation of Task g2 will block until Task g2, and all of g2's descendants, have completed running. As Task g2 can not be started until Task g1 has been completed, we are now guaranteed to generate either the sequence "A1B2C" or the sequence "AB12C". We could further narrow this down to just "A1B2C" by applying the WAIT constraint to Task g1.

The WAIT constraint is quite similar to the behavior generated by invoking a Task as a Function with one critical exception. Tasks that are invoked as a function are not considered when finding the previous SPAWN (or WITH statement) operation for subsequent SPAWN's. Consider:

GOAL bar()

{

WITH ( SERIAL )

{

SPAWN h1();

h2();

SPAWN h3();

}

}



In this case, Task h3 would be serialized to run after Task h1. Task h2 would not be constrained, other than being run immediately, with Task bar blocked until Task h2, and all h2's descendants, had completed. More to the point, Task h1 would not be guaranteed to be completed (or possibly even to be started) prior to Task h2 being invoked.

So far, all of our examples have utilized distinct task names. But when referring to a Task that is invoked multiple times, that Task's name becomes ambiguous. This ambiguity can be resolved by using a C label before the SPAWN statement, and referring to that label in place of the task's name. For example:

GOAL bar()

{

first: SPAWN g1();

second: SPAWN g1();

third: SPAWN g2() WITH SERIAL second;

}





4 Underlying Concepts

TDL, the Task Description Language, is based on TCM, the Task Control Management System library. The TCM library, in turn, is a collection of function calls that build and manage Task-Trees.

Task-Trees form the underlying representation of TCM, and therefore TDL. Task-Trees encode the hierarchical relationship between Parent/Child Tasks/Subtasks, as illustrated in Figure ??. Any Task in the TDL/TCM system has exactly one node placed in the Task-Tree each time it is spawned. Thus, through recursion or iteration, a particular Task could have many node's in the Task-Tree.

Once a Task has been spawned, and it has a node in the Task-Tree that TCM is managing, that Task could spawn more Tasks. There is no requirement that it spawn more Tasks. But if the programmer so desires, a Task is permitted to spawn additional Subtasks. These new Subtasks would then be attached, as children, to the Task that spawned them in the Task-Tree. In this way, the Parent/Child Task/Subtask relationship is maintained in the Task-Tree.

This Parent/Child Task/Subtask relationship becomes more important as we look at constraints. Frequently, it is desirable to place constraints between different tasks, or between different sets of tasks. To facilitate this, TDL/TCM defines three distinct task-sets for every node in the task-tree. These sets are Handling, Expansion, and Execution.

Now, when a task is declared in TDL, or when a task is spawned in TCM, it is declared as being of a particular type. Two of these types are Goal and Command. With the idea being that Goals should be used to plan what the program will do next. And Commands should be used to implement that plan. Thus, the three distinct task-sets, Handling, Expansion, and Execution, are defined as follows:

4.1. Handling

Handling corresponds to the actual running time period of the code that makes up the Task. Handling starts when the Task's code is first invoked. And Handling completes when the Task's code finishes. Handling has absolutely no relationship whatsoever to any Subtasks that are created. Other than the obvious, that a Task's Handling must start before any Subtasks can be created. And therefore, that a Task's Handling must start before any Subtask's Handling can start, as illustrated in figure ??.

4.2. Expansion.

For Goal Tasks, Expansion starts when the Task's Handling first starts. Expansion completes only after the current Goal Task's Handling has completed, and the Handling of all Goal descendants has completed. In this way, Expansion reflects the time period beginning when the first Goal of the Task-subtree starts its Handling, and ending when the last Goal of the Task-subtree completes its Handling, as illustrated in figure ??.

For Command Tasks, Expansion is somewhat meaningless. For this reason, for Command Tasks, Expansion is considered to both start and end as soon as the Command Task is created, before the Command Task ever begins Handling.

4.3. Execution

Execution is much like Expansion, only for Commands rather than Goals.

For Goal Tasks, Execution starts when the first Command descendent starts its Handling. For Command Tasks, Execution starts when that Command Task first starts its Handling.

Execution completes only after the completion of Handling of the current Task and all descendent Tasks, regardless of whether they were Goal or Command Tasks. The reasoning here is that one can not know whether or not a Goal Task will spawn additional Command Tasks prior to the completion of that Goal Task's Handling. Therefore, the Handling of the set of all Command Tasks in the Task-subtree can not be declared completed until all of the Goal Tasks in the Task-subtree have completed their Handling.

In this way, Execution reflects the time period beginning when the first Command of the Task-subtree starts its Handling, and ending when all Commands of the Task-subtree have provably concluded their Handling, as illustrated in figure ??.

In the event where there are no Command Tasks in a task-subtree, Execution is somewhat meaningless. However, the knowledge that there are no Command Tasks in a task-subtree can not be gathered until all the Goal Task descendants in that task-subtree have concluded their Handling. For otherwise, how could we know whether or not one of the remaining Goal-descendants would spawn a Command? For this reason, in the degenerate case where there are no Command Tasks in a task-subtree, Execution may not begin until the Handling of all the Goal Tasks has completed. Or, more concisely, Execution is disabled until the conclusion of Expansion. Whereupon, lacking any other unfulfilled constraints on the Execution phase, the Execution will then both begin and end at the moment Expansion is completed.



5 Task Description Language Overview

The Task Description Language is designed to extend the C++ Language to facilitate the declaration, invocation, and manipulation of concurrent procedures call Tasks. The same Tasks that form the nodes of TCM Task-Trees.

A TDL Task can not be declared inside a C++ Class. Nor may it be declared static. Instead, it must be declared as being of a particular task-type, such as Goal, Command, Monitor, Exception Handler, or Exception. Followed by the Task's name, and its arguments enclosed in parenthesis. There is no return type. Any valid C++ datatype may be used in the arguments, including references, pointers, and default initializers. At a glance, TDL Task declarations look like ordinary C++ function declarations, with the return value type replaced by the task-type, as illustrated below.

GOAL goalTask( int theInt,

const char * const * & theCharPtrPtrRef,

classFoo & theClassFooRef,

int (*(*theFunction)(void*))(const double * &),

int theNumber = 3 + 4 * FOO::CONSTANT )

EXPAND FIRST

{

WITH ( PARALLEL )

{

SPAWN firstSubtask ( theInt + 2,

theCharPtrPtrRef,

theClassFooRef )

WITH DISABLE UNTIL secondSubtask HANDLING ENABLED;



SPAWN secondSubtask ( theFunction,

(theNumber % 2) == 0 ? "a" : "b" )

WITH DISABLE FOR 0:0:7.25,

TERMINATE IN 0:2:3.75 AFTER SELF HANDLING ACTIVE;

}

}



However, after the conclusion of the Task's arguments, things become different. There may be optional Task-Level constraints before the Task's body, such as "EXPAND FIRST". (See section 5.5, "Constraints" and section 5.6, "Task Level Constraints".) And the Task's body itself has additional statements and limitations. Specifically:



5.1. Invoking Tasks as a Function Call.

As a form of syntactic sugar, TDL supplies a means of invoking subtasks exactly as one would invoke a standard C++ function call, as illustrated below. This mechanism blocks the calling Task exactly like an ordinary function call, as if a WAIT constraint had been applied, until the subtask completes.

goalTask( 1, c, aFoo, aFunction );



Task's invoked in this manner are exempt from any constraints, including constraints from enclosing WITH statements, and are not considered as possibilities for PREVIOUS. A Task that is invoked in this manner is otherwise the equivalent of SPAWN'ing the Task with the sole constraint of WAIT.

Subtasks may be invoked as a function call inside both Tasks and ordinary C++ functions/methods. However, inside Tasks, the subtask is added to the TCM Task-Tree as a child of the current Task. Outside of Tasks, it is added to the root node of the TCM Task-Tree.

5.2. The SPAWN Statement

The SPAWN statement acts to create a subtask. It is very similar to a standard C++ function call, as illustrated below, but with two important differences. The TDL keyword "SPAWN" must precede the Task-name. And there can be an optional "WITH" clause following the end-parenthesis, specifying one or more comma-separated constraints. (See section 5.5, "Constraints".)

SPAWN goalTask( 1, c, aFoo, aFunction )

WITH DISABLE FOR 0:0:2.75, WAIT;



It should be noted that SPAWN'ing a Task does not actually start the Task. Rather, it registers a request with TCM to run the specified task at some point in time in the future. That point in time could be immediately. Or it could be some seconds, minutes, or longer into the future. The exact length of this delay time is dependent on the platform, the operating system, the system load on the machine, the TCM implementation being used, and, most importantly, on how long it takes to resolve any constraints that are disabling this task.

Like the mechanism for invoking Tasks as a function call, the SPAWN statement is available inside both Tasks and ordinary C++ functions/methods. And as before, inside Tasks, the subtask is added to the TCM Task-Tree as a child of the current Task. Whereas outside of Tasks, it is added to the root node of the TCM Task-Tree. However, constraints relative to other Tasks, where the other Task is specified by Task-Name or label, are only permitted inside a Task body. (See section 5.5.23, "<tag>".)

5.3. The WITH Statement

The WITH statement acts to establish one or more comma-separated constraints across a set of SPAWN statements, as illustrated below. The constraints in the WITH statement are propagated down to the enclosed SPAWN statements, and act to further constrain those SPAWN statements. (See section 5.5, "Constraints".) (See section 6.3, "Details of the WITH Statement".)

WITH ( SEQUENTIAL EXPANSION, SEQUENTIAL EXECUTION )

{

SPAWN f1();

SPAWN f2();

SPAWN f3();

SPAWN f4();

}



5.4. The Constraint Statement

The CONSTRAINT statement acts to apply a single constraint to a particular Task that is spawned elsewhere in the code. It consists of a Task-name or Task-label followed by a constraint, as illustrated below. It is typically used to apply a particular constraint conditionally to a specific Task. (See section 5.5, "Constraints".)

if ( booleanTest() )

goalTask SERIAL fooTask;

else

goalTask SERIAL barTask;



SPAWN fooTask();

SPAWN barTask();

SPAWN goalTask (1, c, aFoo, aFunction );



The Task referred to in a CONSTRAINT Statement can be a future reference. (See section 5.7, "Task References".)

5.5. Constraints

The Constraint clauses in the SPAWN and WITH statements form the heart of TDL. Constraint clauses consist of a comma separated list of one or more individual constraints. These individual constraints are:

5.5.1. EXPAND FIRST

Implies that this subtask's Expansion must end prior to this subtask's Execution beginning. For Command Tasks, this is meaningless. But for Goal Tasks, EXPAND FIRST implies that this Goal subtask, and all Goal descendants of this Goal subtask, must complete their Handling prior to any Command descendants being started.

5.5.2. DELAY EXPANSION

Acts to prevent this subtask's Expansion from starting until such a time as this subtask's Execution is permitted to start. By itself, DELAY EXPANSION has no effect. However, when combined with other constraints that limit when this subtask's Execution may start, DELAY EXPANSION acts to limit when this subtask's Expansion may start.

5.5.3. SEQUENTIAL HANDLING [ <tag> ]

Implies that this subtask's Handling may not start until the Tag-Task's Handling has completed. That is to say, until the Tag-Task's code itself has finished running. But not necessary before or after the Tag-Task's subtask's have started or finished running.

If <tag> is not specified, "PREVIOUS" is used by default. If there is no previous task, this constraint has no effect.

SEQUENTIAL HANDLING is equivalent to DISABLE HANDLING UNTIL <tag> HANDLING COMPLETED.

5.5.4. SEQUENTIAL EXPANSION [ <tag> ]

Implies that this subtask's Expansion may not start until the Tag-Task's Expansion has completed.

That is to say, if the Tag-Task is a Goal Task, it delays this subtask from running (starting Handling, or having its descendants start their Handling) until the Tag-Task has completed its Handling, and any Goal descendants started by the Tag-Task have completed their Handling.

If the Tag-Task is a Command Task, this constraint does absolutely nothing.

If <tag> is not specified, "PREVIOUS" is used by default. If there is no previous task, this constraint has no effect.

SEQUENTIAL EXPANSION is equivalent to DISABLE EXPANSION UNTIL <tag> EXPANSION COMPLETED.

5.5.5. SEQUENTIAL EXECUTION [ <tag> ]

Implies that this subtask's Execution may not start until the Tag-Task's Execution has completed.

That is to say, if this subtask is a Goal Task, Sequential Execution delays any Command descendants of this (Goal) subtask from starting their Handling until the Tag-Task, and all of the Tag-Task's descendants, have concluded being handled. Sequential Execution does not prevent this (Goal) subtask itself from being handled, or any Goal descendants of this (Goal) subtask from being handled.

If this subtask is a Command Task, Sequential Execution delays the (Command) subtask itself, along with all of the (Command) subtask's descendants, from starting their Handling until the Tag-Task, and all of the Tag-Task's descendants, have concluded being handled.

If <tag> is not specified, "PREVIOUS" is used by default. If there is no previous task, this constraint has no effect.

SEQUENTIAL EXECUTION is equivalent to DISABLE EXECUTION UNTIL <tag> EXECUTION COMPLETED.

5.5.6. SERIAL [ <tag> ]

Implies that this subtask's Handling may not start until the Tag-Task's Execution has completed.

That is to say, that this subtask's Handling, and the Handling of all the descendants of this subtask, may not start until the Tag-Task's Handling has completed. And until all the descendants of the Tag-Task have completed their Handling.

If <tag> is not specified, "PREVIOUS" is used by default. If there is no previous task, this constraint has no effect.

SERIAL is equivalent to DISABLE UNTIL <tag> EXECUTION COMPLETED.

5.5.7. PARALLEL

PARALLEL is the default constraint. It reflects an absence of other constraints. PARALLEL exists for completeness, and to serve as a means of breaking prior constraints in nested WITH statements. (See Section 6.3.2, " Nested WITH Statements".)

PARALLEL refers only to constraints between SPAWN'ed subtasks. It never refers to means by which embedded C or C++ code will be executed. (See Section 6.1, "Embedded C/C++ code".)

5.5.8. WAIT

Implies that the code involved in actually SPAWN'ing this subtask should block (not return) until this subtask has completed its Handling, and all the descendants of this subtask have completed their Handling.

WAIT makes the subtask SPAWN'ing operation almost the equivalent of a function call. But not quite, since any other constraints that this subtask has will need to be fulfilled prior to this subtask being started. Which could result in the current Task's Handling blocking until Tasks other than this subtask have concluded their Handling.

5.5.9. DISABLE [ <constraint-option> ] UNTIL <event>

Implies that this subtask's Handling, Expansion, or Execution will not be permitted to occur until the <event> has transpired. (See Section 5.4.17, "Event".)

If <constraint-option> is not specified, neither this subtask, nor any of this subtask's descendants, are permitted to start their Handling until the <event> has transpired.

5.5.10. DISABLE [ <constraint-option> ] UNTIL <absolute-time>

Implies that this subtask's Handling, Expansion, or Execution will not be permitted to occur prior to <absolute-time>. (See Section 5.4.18, "Absolute Time".)

If <constraint-option> is not specified, neither this subtask, nor any of this subtask's descendants, are permitted to start their Handling prior to <absolute-time>.

5.5.11. DISABLE [ <constraint-option> ] FOR <relative-time> [AFTER <event>]

Implies that this subtask's Handling, Expansion, or Execution will not be permitted to occur until at least <relative-time> after the moment that the <event> has transpired. (See Section 5.4.19, "Relative Time".) (See Section 5.4.17, "Event".)

If the optional "AFTER" clause is not present, and there is no <event>, the <event> is considered to be the creation (spawning) of this subtask. And this subtask's Handling, Expansion, or Execution will not occur until at least <relative-time> after the moment that this subtask was created (spawned).

If <constraint-option> is not specified, neither this subtask, nor any of this subtask's descendants, are permitted to start their Handling prior to <relative-time> after the <event>.

5.5.12. TERMINATE AT <event>

Implies that this subtask will be terminated after the <event> has transpired. (See Section 5.4.17, "Event".)

5.5.13. TERMINATE AT <absolute-time>

Implies that this subtask will be terminated after <absolute-time>. (See Section 5.4.18, "Absolute Time".)

5.5.14. TERMINATE IN <relative-time> [ AFTER <event> ]

Implies that this subtask will be terminated after <relative-time> after the moment that the <event> has transpired. (See Section 5.4.19, "Relative Time".) (See Section 5.4.17, "Event".)

If the optional "AFTER" clause is not present, and there is no <event>, the <event> is considered to be the creation (spawning) of this subtask. And this subtask will be terminated after <relative-time> after the moment that this subtask was created (spawned). A point in time which could be substantially earlier than when this subtask actually starts its Handling.

5.5.15. ACTIVATE AT <event>

This constraint may only be applied to a Monitor Task. It implies that this Monitor subtask should start running (be activated) when <event> has transpired. (See section 5.5.? "Event", section 5.10 "Monitors".)

5.5.16. ACTIVATE AT <absolute-time>

This constraint may only be applied to a Monitor Task. It implies that this Monitor subtask should start running (be activated) after <absolute-time>. (See section 5.5.? "Absolute Time", section 5.10 "Monitors".)

5.5.17. ACTIVATE IN <relative-time> [ AFTER <event> ]

This constraint may only be applied to a Monitor Task. It implies that this Monitor subtask should start running (be activated) after <relative-time> after the moment that the <event> has transpired. (See section 5.5.?, "Relative Time", section 5.5.? "Event", section 5.10 "Monitors".)

If the optional "AFTER" clause is not present, and there is no <event>, the <event> is considered to be the creation (spawning) of this Monitor subtask. And this subtask will be activated after <relative-time> after the moment that this subtask was created (spawned).

5.5.18. MAXIMUM ACTIVATE <int>

This constraint may only be applied to a Monitor or Exception Handler Task. It implies that, after this Task has been activated the specified maximum number of times, it may not be activated again. Effectively terminating this Monitor Task, or removing this Exception Handler Task. (See section 5.10 "Monitors", section 5.11 "Exceptions".)

5.5.19. MAXIMUM TRIGGER <int>

This constraint may only be applied to a Monitor Task. It implies that, after this Monitor Task has had TRIGGER() invoked the specified maximum number of times, this Monitor Task may not be activated again. Effectively terminating this Monitor Task. (See section 5.10, "Monitors".)

5.5.20. PERIOD <relative-time>

This constraint may only be applied to a Monitor Task. It implies that this Monitor task should be activated periodically with the specified time interval. The Monitor Task will continuously reactive until MAXIMUM ACTIVATE or MAXIMUM TRIGGER expire, or until the Monitor Task is terminated. (See section 5.10, "Monitors".)

5.5.21. [ EXCEPTION ] HANDLER <Exception-Handler-Name> ( <arguments> )

This constraint adds an exception handler to the Task that is currently being spawned. The <Exception-Handler-Name> is the name of an Exception Handler Task. The arguments can be any standard C++ function/method arguments, with the number and type specified by the Exception Handler Task's definition. (See section 5.11, "Exceptions".)



Evaluation of the expressions composing the arguments occurs when this Exception Handler Constraint is declared. As this may cause confusion for inexperienced users, TDL explicitly disallows the use of Exception Handler Constraints as part of WITH Statement Constraints (see section 5.3, "The WITH Statement"). Exception Handler Constraints may only be used as part of SPAWN Statements (see section 5.2, "The SPAWN Statement"), Constraint Statements (see section 5.4, "The Constraint Statement"), and Task-Level Constraints (see section 5.6, "Task Level Constraints"). More experienced users may circumvent this idiotic limitation by utilizing a Constraint Statement to attach an Exception Handler Constraint to a labeled WITH Statement.

5.5.22. Handles <Exception-Task-Name>

This is not a real constraint. It is attached to Exception Handler Tasks in among their Task-Level constraints. There must be exactly one Handles constraint for each Exception Handler Task, specifying which Exception Task that Exception Handler Task is handling. (See section 5.11, "Exceptions".)

5.5.23. <tag>

<tag> is a reference to another Task. It can be any one of the following:



5.5.24. <constraint-option>

<constraint-option> refers to one of the following keywords:



5.5.25. <event>: <tag> [ <constraint-option> ] <state>

An <event> refers to the achievement a particular state for the Tag-Task's Handling, Expansion, or Execution. The state must be one of following keywords:



5.5.26. <absolute-time>

<absolute-time> refers to a particular time of day in the form of "Hours : Minutes : Seconds . Fractions_of_a_Second". <absolute-time> is specified in military time, with the hours ranging from 0 to 23 inclusive. And both the minutes and seconds ranging from 0 to 59 inclusive. Both hours and minutes are required fields. Seconds and any fractions of a second, along with the corresponding ':' and '.', are optional.

5.5.27. <relative-time>

<relative-time> refers to a point in time that is <relative-time> into the future. Like <absolute-time>, <relative-time> is of the form "Hours : Minutes : Seconds . Fractions_of_a_Second". However, unlike <absolute-time>, the only limitation on the hours, minutes, seconds and fractions is that they be greater than or equal to zero. Also, seconds are the required field. With hours, minutes, and any fractions of a second, along with the corresponding ':', ':', and '.', being optional.



5.6. Task Level Constraints

A Task Definition can include a comma-separated list of one or more constraints that are applied to the Task every time it is SPAWN'ed. This list of constraints, if present, occurs immediately after the closing parenthesis of the Task's arguments, and immediately before the opening brace that starts the Task's body. The precise constraints that are permitted are dependent upon the Task's type. Constraints that are relative to other Tasks are never permitted.

Goal and Command Tasks support EXPAND FIRST, DELAY EXPANSION, and EXCEPTION HANDLER Task-Level constraints. Monitor Tasks support these as well, along with the MAXIMUM ACTIVATE, MAXIMUM TRIGGER, and PERIOD constraints.

Exception Tasks do not support any Task-Level constraints. However, they do support an inheritance mechanism. (See section 5.11, "Exceptions".)

Exception Handler Tasks support only the HANDLES and MAXIMUM ACTIVATE constraints as Task-Level constraints.

5.7. Task References

Some constraints refer to other Tasks. For example, you can serialize a Task to run after another Task has completed. These references to other Tasks may be specified through the keywords SELF, PARENT, and PREVIOUS, or through Task names and labels. (See Section 5.5.23, "<tag>".)

The keyword SELF refers to the current Task that is being spawned. The keyword PARENT refers to the parent (enclosing) Task that is spawning the current Task. The keyword PREVIOUS refers to the last child-Task that the parent (enclosing) Task spawned. (See Section 6.2, "The Task-Name Previous", for PREVIOUS's special effects when utilized as part of a WITH Statement.) These keywords may be used inside both Tasks and ordinary C/C++ functions and methods. Outside of Tasks, the parent (enclosing) Task is considered to be the TCM Root Node.

Task name and label references can only be used inside the body of a Task. Task names may only be used if that Task-name uniquely identifies the precise Task that one is referring to. If the same Task is spawned in multiple locations, its name does not uniquely refer to a single Task spawning. In this case, it is necessary to prepend a C-style label immediately before the spawning of the Task that one wishes to refer to. One can then refer to that Task by its label. Multiple labels are permitted and accepted.

If the Task being referred to does not get executed for whatever reason, including conditional statements, any constraints based upon that Task will be considered fulfilled as soon as it can be determined that the referred-to Task is not being executed. This determination is resolved dynamically, at runtime, through extra code added by the TDL Compiler.

5.7.1. Future References

A Task may be referred to, through its Task name or labels, before it is spawned. This is considered valid in TDL. One must be careful, however, in using this feature to avoid creating mutual dependency loops that prevent any of the Tasks from running. (Ie: A serial B, B serial A.)

This example, while somewhat confusing, this is legitimate TDL code:

GOAL foo()

{

SPAWN f(1) WITH SERIAL f1, SERIAL f3;

SPAWN f(2) WITH SERIAL f2, SERIAL f3;

if ( booleanTest() )

f1: f2: SPAWN f(0);

else

f3: SPAWN f(3);

}



5.8. #using

Both C and C++ support the concept of breaking up large programs into multiple smaller subcomponents. This permits one to utilize just the subcomponents, or header files, that one needs through #include.



The TDL Compiler inherently splits TDL program files up into two files, creating both a C++ header and a C++ source file. When it creates these files, any #include's that occur in the TDL file are reproduced only in the generated C++ source file.



Occasionally, one wishes to include files in the generated C++ header file. In these situations, the #using statement is used. #using operates analogously to #include, and is translated by the TDL compiler into a #include statement in the generated C++ header file. That header file is, of course, automatically #include'd by the generated C++ source file.



#using translates file names ending with '.tdl' into the appropriate generated C++ header file suffix. This enables one to invoke #using directly on another TDL file, and thereby include that TDL file's corresponding generated C++ header file. For example:



#using <foo.tdl>

#using "bar.tdl"

5.9. Jump statements

Jump statements alter the flow of control in the program. In C and C++, jump statements include break, continue, return, and goto. TDL does not support the goto statement. However, it does support break, continue, and return. And TDL adds Success, Fail, and Postpone.

5.9.1. Success



In TDL, Success is equivalent to return. Both indicate that a Task has completed its processing successfully. And both act to terminate the current (enclosing) Task's handling.

5.9.2. Fail [ <exception-name> ( arguments ) ]



Fail is, in many ways, analogous to the C++ throw statement. It terminates the current (enclosing) Task's handling. And it throws the specified exception up the Task Tree. If no exception is specified, a TDL_BaseException is used. Fail will cause the Task Tree to be traversed, going from child to parent, until an ancestor is found who has an exception handler capable of handling the specified exception. (See Section 5.11, "Exceptions".) If the current (enclosing) Task has a suitable exception handler, it will be utilized.

5.9.3. Postpone



Occasionally, it is useful for a Task to return without the Task's handling being completed. Postpone fulfills this functionality. When a Task is Postpone'd, a C/C++ return is executed. However, the Task is not marked as completed. Instead, it is left as 'pending' until one invokes TDL_SUCCESS on that Task's TCM_Task_Tree_Ref elsewhere in the code. The Task's TCM_Task_Tree_Ref can be obtained by invoking TDL_REF ( PARENT ) inside the Task prior to Postpone'ing it. (See Section 5.12, "TDL_REF".) For example:



const TCM_Task_Tree_Ref & task;



GOAL foo()

{ task = TDL_REF ( PARENT );

POSTPONE;

}



void foo_completion()

{

TDL_SUCCESS ( task );

}



Postpone is primarily utilized as a means for interfacing with other systems that provide a delayed response, such as event driven systems. It permits other Tasks to continue their processing while, at the same time, waiting for pending events. Naturally, Tasks that are constrained on the completion of a Postpone'd Task will not be able to run until the proper invocation of TDL_SUCCESS.



5.10. Monitors



Monitors are repeating Tasks. Unlike Goals and Commands, which are typically run only once each time they are SPAWN'ed, the code in a Monitor Task can be run and rerun multiple times every time that Monitor is SPAWN'ed. Each time a Monitor Task is rerun, it is considered to be a separate instance of that SPAWN'ed Monitor. These instances form independent subtasks of that SPAWN'ed Monitor.



Periodic (or Polling) Monitors have an associated time period. These monitors are restarted, in much the same way as a Goal or Command is SPAWN'ed, automatically whenever their time period elapses. This time period is measured relative to the start of the previous instance of the SPAWN'ed Monitor. As such, the time period forms a lower rather than an upper or exact bound on when subsequent instances of that SPAWN'ed Monitor are restarted. (See the limitations in Section 5.2, "The Spawn Statement".)



Non-Polling Monitors do not have an associated time period. They are started and restarted through the use of the ACTIVATE constraint. The ACTIVATE constraint may be applied in the WITH clause when the Non-Polling Monitor is SPAWN'ed. Or it may be applied through a WITH Statement. But, more commonly, it is applied through a Constraint Statement. (See Section 5.4, "The Constraint Statement".)



It is possible for multiple instances of a Monitor to be running concurrently. One can avoid this by utilizing the SERIAL Task Level Constraint when the Monitor is defined. SERIAL, in this case, applies between the Monitor's instances. Thereby guaranteeing that the preceding instance of the Monitor will finish prior to any subsequent instances of the Monitor being permitted to start.





Whenever a Monitor is restarted, that instance of the Monitor is automatically passed a copy of the Monitor's arguments on its stack. If those arguments are modified, subsequent instances of the Monitor will not see those modifications. Unless, of course, the Monitor's arguments contain a pointer, and it is what that pointer points to that is actually modified.



Using pointers in this fashion, to pass information between instances of the same Monitor, is somewhat clumsy and inefficient in terms of programmatical overhead. Moreover, it is frequently desirable for Monitors to change their behavior in future instances based upon what they are currently observing.



So Monitors have a special function available only to them. It is called TRIGGER(). TRIGGER() increments a counter, the value of which can be obtained through another special function called getNumberOfTriggers(). This permits Monitors to be written containing a switch statement based upon the value of getNumberOfTriggers(). Thereby allowing future instances of those Monitors to migrate to the next behavior by simply invoking TRIGGER().



Monitors have a third special function available only to them. getNumberOfActivates() returns the number of instances of that Monitor that have been SPAWN'ed (restarted) so far.



Unlike Goals or Commands, which are considered to be completed when they and all of their children have finished their handling, Monitors are only considered to be completed when they have exceeded their maximum number of activates or their maximum number of triggers. The only other way to end a Monitor Task is to TERMINATE it.



A Periodic (Polling) Monitor could look like this: (Although usually Monitors perform some more useful action than merely printing messages.)



Monitor foo ( )

serial, period 0:0:1.0, maximum activate 4, maximum trigger 4

{

cout << "foo: triggers = " << getNumberOfTriggers()

<< ", activates = " << getNumberOfActivates() << endl;

TRIGGER();

}



5.10.1. TRIGGER()



TRIGGER() is a special function that is only available inside Monitor Tasks. It increments a counter that can be read by any instance of that Monitor through getNumberOfTriggers(). If the maximum triggers for a Monitor Task is reached or exceeded, that Monitor Task is considered completed, and no further instances will be created (restarted).

5. 5.10.2. getNumberOfTriggers()



getNumberOfTriggers() is a special function that is only available inside Monitor Tasks. It returns the number of times instances of this particular Monitor have invoked "TRIGGER()".

5.10.3. getNumberOfActivates()



getNumberOfActivates() is a special function that is only available inside Monitor Tasks. It returns the number of instances of this particular Monitor that have been created (restarted).



5.11. Exceptions



Exceptions provide a means for incrementally adding error recovery to TDL software. The general idea is that TDL Tasks, upon seeing unexpected situations, can generate Exceptions. Those Exceptions will then travel upwards through the Task-Tree, starting with the current Task of course, searching for suitable Exception Handlers. Exception handlers can then be added, incrementally, to appropriate Tasks to intercept these Exceptions and deal with the problems that they represent.



Exception Handlers can be attached to any Goal, Command, or Monitor Task. Each Exception Handler is written to intercept a single specific Exception Task, or any Exception Tasks that are derived from that single specific Exception Task.



Exception Tasks and Exception Handler Tasks are analogous to, but not quite the same as, C++ try/throw/catch statements. The C++ exception mechanism deals with exceptions by performing long-jumps, and thus unrolling the stack. This results in the code resuming execution at the point of the catch statement, with all the local variables of the enclosing routine still available.



In TDL, Tasks are run concurrently. And children can be run concurrently with their parent. So Exception Handlers must be able to run both concurrently and asynchronously. Also, as the descendants of a Task can continue running long after that Task's handling has been completed, so must a Task's Exception Handlers be able to run long after their Task's handling has been completed. These constraints preclude access to the local variables of the Task inside an Exception Handler.



But users frequently wish to have local variables available from the point at which the Exception Handler was established, as well as from the point at which the Exception was generated. TDL supports both of these through arguments to both the Exception Handler Task and the Exception Task. These argument lists are defined by the user just as they are for any other Task. With the Exception Task's arguments being available inside the Exception Handler Task through an instance of a structure named with the Exception Task's name.



If an Exception Handler Task is unable to deal with a particular exception that it receives, it can re-Fail that Exception. The Exception will then continue to search upwards through the Task-Tree until it finds another suitable Exception Handler Task.



For example:



Exception BaseException ( const char * theBaseFailLocation );



Exception DerivedException ( const char * theDerivedFailLocation,

int theExceptionNumber = 1 )

: BaseException ( theDerivedFailLocation );



Exception Handler BaseExceptionHandler ( const char * theHandlerLocation )

Handles BaseException

{

cout << "BaseExceptionHandler.: " << theHandlerLocation << endl

<< "BaseException........: " << BaseException.theBaseFailLocation

<< endl;

}



Exception Handler DerivedExceptionHandler ( const char * theHandlerLocation,

int theHandlerNumber = 1)

Handles DerivedException

{

cout << "DerivedExceptionHandler.: " << theHandlerLocation

<< ", " << theHandlerNumber << endl

<< "DerivedException........: "

<< DerivedException.theDerivedFailLocation

<< ", " << DerivedException.theExceptionNumber << endl;

FAIL; /* Re-Throw this Exception. */

}



Goal Test1()

Exception Handler BaseExceptionHandler ( "Test1()" )

{

cout << "Test1()" << endl;

SPAWN Test2 ( 0 );

}



Command Test2 ( int theIndex )

Exception Handler DerivedExceptionHandler ( "Test2()", theIndex + 1 )

{

cout << "Test2(" << theIndex << ")" << endl;

FAIL DerivedException ( "FAIL'ed in Test2()", theIndex + 2 );

}



Generates the output:



Test1()

Test2(0)

DerivedExceptionHandler.: Test2(), 1

DerivedException........: FAIL'ed in Test2(), 2

BaseExceptionHandler.: Test1()

BaseException........: FAIL'ed in Test2()



As the DerivedException is handled first through the DerivedExceptionHandler attached to Command Test2, and then through the BaseExceptionHandler attached to Goal Test1.

5.11.1. Exception Tasks



Exception Tasks have no Task body. They have no associated code. Instead, Exception Tasks consist of a set of data, their arguments, that are passed upwards whenever this exception is generated through the FAIL command.



Exception Tasks may optionally inherit from a single base Exception Task using a syntax similar to C++ inheritance. If a base Exception Task is not specified, the TDL_BaseException base Exception Task, which takes no arguments, is assumed. When a base Exception Task is specified, all the necessary arguments to that base Exception Task must also be specified using arguments from the current Exception Task and/or constant expressions.



For example:



Exception BaseException ( const char * theBaseFailLocation );



Exception DerivedException ( const char * theDerivedFailLocation,

int theExceptionNumber = 2 )

: BaseException ( theDerivedFailLocation );

5.11.2. Exception Handler Tasks



Exception Handler Tasks have a Task body. They contain the code that handles a single specified Exception Task, or any Exception Tasks that are derived from that single specified Exception Task. More than one Exception Handler Task can be written to handle the same Exception Task. And those Exception Handler Tasks can be attached to different Tasks in the Task-Tree.



Exception Handler Tasks have their handled Exception Task's arguments available inside them through an instance of a structure named with the Exception Task's name. Exception Handler Tasks can reissue their Exception Task by using the FAIL command with no arguments. This permits other Exception Handler Tasks the opportunity to handle that Exception Task.



For example:



Exception Handler DerivedExceptionHandler ( const char * theHandlerLocation,

int theHandlerNumber = 1)

Handles DerivedException

{

cout << "DerivedExceptionHandler.: " << theHandlerLocation

<< ", " << theHandlerNumber << endl

<< "DerivedException........: "

<< DerivedException.theDerivedFailLocation

<< ", " << DerivedException.theExceptionNumber << endl;

FAIL; /* Re-Throw this Exception. */

}

5.11.3. FAIL



FAIL is the means by which Exception Tasks are generated. It is analogous to throw in the C++ exception mechanism.



FAIL terminates the handling of the current Task. If an Exception Task is not specified after the FAIL keyword, a TDL_BaseException is thrown. The exception to this rule occurs inside Exception Handler Tasks, where the lack of an Exception Task after the FAIL keyword causes the current Exception Task to be re-FAIL'ed (rethrown).



An example of using FAIL would be:



FAIL DerivedException ( "FAIL'ed in Test2()", theIndex + 2 );



5.12. TDL_REF ( <name> )

Occasionally, it is useful to access the underlying TCM library directly. TDL_REF() is a macro that returns an object which can be cast to a const TCM_Task_Tree_Ref &. In this manner, one can use ((const TCM_Task_Tree_Ref &) TDL_REF ( foo ) ) to access the TCM_Task_Tree_Ref that corresponds to the SPAWN'ed subtask foo.



TDL_REF may only be used inside Tasks. TDL_REF may be used to refer to Tasks before they are actually SPAWN'ed. (See section 5.12.1, "Future References".) The Task-name argument to TDL_REF must not be in quotes. TDL_REF ( PARENT ) is a special case that refers to the current Task.



The object returned by TDL_REF supports iostreams. Saying "cerr << TDL_REF ( foo );" is valid, and will print out the current information available on the foo Task to stderr.



6 Task Description Language Details

6.1. Embedded C/C++ code

One of the greatest areas of potential confusion in the TDL language is the way in which TDL constraints may be interpreted, and mistakenly applied to, embedded C or C++ code.

TDL was intended as a means of creating and manipulating concurrent procedures known as Tasks. To facilitate this, Tasks may contain the TDL extensions SPAWN, WITH, and so forth. However, to facilitate actually accomplishing some programmatic achievement other than merely creating Tasks, Tasks may contain most of the features of the standard C/C++ languages. However, the TDL extensions, especially the constraints, do not apply to the standard C/C++ code. The TDL extensions apply solely to the subtasks that are created.

For example, consider:

WITH ( PARALLEL )

{

int I = 0, j = 0;



SPAWN z1 ( I++ , j );



j++;



SPAWN z2 ( I , j );

}



The PARALLEL constraint applies to the z1 and z2 subtasks only. It permits both z1 and z2 to be run concurrently, in parallel. The PARALLEL constraint is not applied to the declaration of I and j as integers. Nor is the parallel constraint applied to the initialization of I and j with 0. Nor is the PARALLEL constraint applied to I++. Nor is the parallel constraint applied to j++. And z1 will always be SPAWN'ed (created) before z2.

The distinction supplied by the PARALLEL constraint is that subtask z2 will be permitted to run prior to the completion, or possibly even the start, of subtask z1.

Programmers must strive to remember that SPAWN'ing a Task does not actually start a Task. SPAWN'ing merely registers a Task to be run. And constraints are only applied to subtasks that have been registered to be run. Constraints are never applied directly to embedded C or C++ code.

6.2. The Task-Name Previous

PREVIOUS was intended as a convenience mechanism for sequentially chaining tasks with a constraint. For example, consider:

SPAWN p1();

SPAWN p2() WITH SERIAL p1;

SPAWN p3() WITH SERIAL p2;

SPAWN p4() WITH SERIAL p3;



Which could be replaced with:

SPAWN p1();

SPAWN p2() WITH SERIAL PREVIOUS;

SPAWN p3() WITH SERIAL PREVIOUS;

SPAWN p4() WITH SERIAL PREVIOUS;



Or, since PREVIOUS is the default Tag-Task, with:

SPAWN p1();

SPAWN p2() WITH SERIAL;

SPAWN p3() WITH SERIAL;

SPAWN p4() WITH SERIAL;



Or, by using a WITH statement, with:

WITH ( SERIAL )

{

SPAWN p1();

SPAWN p2();

SPAWN p3();

SPAWN p4();

}



6.2.1. Use of Previous in a WITH Statement Constraint

However, utilization of PREVIOUS in a WITH statement introduces a potential problem. Specifically, for the first Task in a WITH statement, PREVIOUS is unable to refer to any other Task in the scope of that WITH statement. Consider the case:

SPAWN p0();

WITH ( SERIAL PREVIOUS )

{

SPAWN p1();

SPAWN p2();

}



The question that arises is how to apply the "SERIAL PREVIOUS" constraint to Task p1. We resolve this ambiguity in TDL by declaring that PREVIOUS constraints in a WITH statement must resolve to prior Tasks inside the scope of that WITH statement. If there is no prior Task inside that WITH statement, the constraint is ignored. So this case is equivalent to:

SPAWN p0();

SPAWN p1();

SPAWN p2() WITH SERIAL PREVIOUS;



Ie:

SPAWN p0();

SPAWN p1();

SPAWN p2() WITH SERIAL p1;



It should be noted that any constraints that are inside a SPAWN statement's constraint clause do not suffer this scoping restriction. And as such, those constraints are always applied. For example:

SPAWN q0();

WITH ( SERIAL )

{

SPAWN q1() with SEQUENTIAL HANDLING PREVIOUS;

SPAWN q2();

}



Would be the equivalent of:

SPAWN q0();

SPAWN q1() WITH SEQUENTIAL HANDLING PREVIOUS;

SPAWN q2() WITH SERIAL PREVIOUS;



Ie:

SPAWN q0();

SPAWN q1() WITH SEQUENTIAL HANDLING q0;

SPAWN q2() WITH SERIAL q1;



6.2.2. When PREVIOUS is a WITH Statement:

Another area of ambiguity gets introduced when PREVIOUS refers to a WITH statement. For example:

WITH ( PARALLEL )

{

SPAWN r1();

SPAWN r2();

}

SPAWN r3() WITH SERIAL PREVIOUS;



Should r3 be constrained with SERIAL r1 or r2? Since the information as to which Task, r1 or r2, will complete first is unknown, we resolve this ambiguity by treating the WITH statement as a set of SPAWN'ed tasks, and stating that r3 is constrained on all the SPAWN'ed tasks inside that set. So this example would be equivalent to:

SPAWN r1();

SPAWN r2();

SPAWN r3() WITH SERIAL r1, SERIAL r2;



6.2.3. When PREVIOUS is an empty WITH Statement:

This creates a fencepost ambiguity that occurs when the preceding SPAWN or WITH statement is a WITH statement that does not contain any SPAWN'ed subtasks. For example:

SPAWN s1();

WITH ( SEQUENTIAL HANDLING PREVIOUS )

{

}

SPAWN s2() WITH SERIAL PREVIOUS;



We resolve this fencepost ambiguity by considering PREVIOUS to be a chain. A WITH statement that does not actually contain any SPAWN'ed subtasks could potentially have the effect of breaking a PREVIOUS chain. Consider:

WITH ( SERIAL PREVIOUS )

{

SPAWN s1();

WITH ( SEQUENTIAL HANDLING PREVIOUS )

{

}

SPAWN s2();

}



In this case, if we permit a WITH statement that does not contain any SPAWN'ed subtasks to break the PREVIOUS chain, we would have s2 running in parallel with s1, rather than sequentially. Since this situation is extremely undesirable, we resolve this ambiguity by stating that WITH statements that do not contain any SPAWN'ed subtasks are not considered when deciding the value of PREVIOUS. Thus, both of the above examples are equivalent to:

SPAWN s1();

SPAWN s2() with SERIAL s1;



6.2.4. Static versus Dynamic Binding of PREVIOUS:

Unfortunately, this solution raises another ambiguity. How can any TDL compiler know statically, at compile time, whether or not every WITH contains at least one SPAWN'ed subtask. Consider the following:

SPAWN t1();

WITH ( SERIAL PREVIOUS )

{

if ( testValue ( argumentPassedInFromuser ) )

SPAWN t2();

}

SPAWN t3();



This is merely one case of a more general ambiguity. It's not just a matter of detecting whether or not a WITH statement contains at least one SPAWN'ed subtask. It's a matter of resolving which SPAWN'ed subtask PREVIOUS refers to. Consider this:

WITH ( SERIAL PREVIOUS )

{

if ( testValue ( argumentPassedInFromuser ) )

SPAWN u1();

else

SPAWN u2();

SPAWN u3();

}



Should u3 be constrained by "SERIAL u1" or "SERIAL u2"? There is no way for any TDL compiler to know this statically, at compile time. For this information does not exist at compile time. And the decision as to which constraint (SERIAL u1 or SERIAL u2) should be applied might even vary between different invocations of the final executable.

So in TDL, we resolve this ambiguity, for both the question of which task, as well as the question of whether or not a WITH statement contains at least one SPAWN'ed subtask, by declaring that the value of PREVIOUS is dynamically bound at runtime. Which is to say that the value of PREVIOUS is capable of varying between different invocations of the final executable, should conditionals in the code change which Tasks are SPAWN'ed.

6.3. Details of the WITH Statement.

A WITH statement is more than just a set of constraints that are applied to every SPAWN statement that it contains. The WITH Statement is a set of SPAWN statements.

It is, in effect, almost a new entry into the Task-Tree. Almost, but not quite, since the WITH Statement does not actually generate a new Task-Tree entry. But rather, the effect, from the programmers perspective, by considering the union of the Handling, Expansion, and Execution of all the subtasks inside the WITH statement's body, is quite similar.

Once crucial difference is that the Handling of a WITH statement's body code can never be delayed. However, the Handling, Expansion, and Execution of all the subtasks SPAWN'ed inside the WITH statement's body can be delayed. And just as easily, other subtasks, outside of the WITH statement's body, can be delayed until the Handling, Expansion, and/or Execution of all the subtasks SPAWN'ed inside the WITH statement have been enabled, activated, or completed. For example:

SPAWN w1();

WITH ( SERIAL w1 )

{

SPAWN w2();

SPAWN w3();

}

SPAWN w4() WITH SERIAL PREVIOUS;



Is equivalent to:

SPAWN w1();

SPAWN w2() WITH SERIAL w1;

SPAWN w3() WITH SERIAL w1;

SPAWN w4() WITH SERIAL w2, SERIAL w3;



This is what is meant by considering all of the SPAWN statements inside a WITH statement body to be set, an entity in and of itself. The concept is that the set of all SPAWN statements inside the WITH statement can form a single entity that can be constrained, or referred to inside constraints on other Tasks.

Unfortunately, in considering all of the SPAWN statements inside a WITH statement body to be set, an entity in and of itself, we create certain ambiguities.

6.3.1. WITH Statement Constraints:

Most constraints will combine conjunctively. In the previous example, Task w4 is disabled until Task w2 AND Task w3 have completed. Achieving the effect of a new Task-Tree entry.

However, certain constraints combine disjunctively. Specifically, the TERMINATE constraints. For example:

WITH ( PARALLEL )

{

SPAWN x1();

SPAWN x2();

}

SPAWN x3

WITH TERMINATE AT PREVIOUS EXECUTION COMPLETED;



Is equivalent to:

SPAWN x1();

SPAWN x2();

SPAWN x3()

WITH TERMINATE AT x1 EXECUTION COMPLETED,

TERMINATE AT x2 EXECUTION COMPLETED;



With the result that Task x3 will be terminated after Task x1 or Task x2 has been completed execution. But not necessarily after both have completed execution.

6.3.2. Nested WITH Statements

Another area of ambiguity refers to how we should handle nested WITH statements. For example:

WITH ( constraint-1 )

{

WITH ( constraint-2 )

{

SPAWN a1();

}

}



Should this be equivalent to:

SPAWN a() WITH constraint-1, constraint-2;



And if so, what happens in the case:

WITH ( SERIAL )

{

WITH ( PARALLEL )

{

SPAWN a1();

SPAWN a2();

}

}



Should Task's a1 and a2 run sequentially or concurrently? We resolve this ambiguity by stating that nested WITH statements do not automatically inherit any enclosing WITH statement's constraints. Rather, the nested WITH statement is treated as a set, as an independent singular entity of the enclosing WITH statement's body. For example:

W1: WITH ( SEQUENTIAL HANDLING PREVIOUS )

{

SPAWN b1();

W2: WITH ( PARALLEL )

{

SPAWN b2();

SPAWN b3();

}

SPAWN b4();

}



Is equivalent to:

SPAWN b1();

SPAWN b2() WITH SEQUENTIAL HANDLING b1;

SPAWN b3() WITH SEQUENTIAL HANDLING b1;//*NOT* b2

SPAWN b4() WITH SEQUENTIAL HANDLING b2,

SEQUENTIAL HANDLING b3;



Specifically, when the SEQUENTIAL HANDLING constraint was applied to the W2 WITH statement, the W2 WITH statement was treated as a set, as a singular object. Thus, the subtasks SPAWN'ed inside W2 are all constrained by SEQUENTIAL HANDLING PREVIOUS, where PREVIOUS is the previous SPAWN'ed task in the W1 WITH statement. And not the previous SPAWN'ed task in the W2 WITH statement.

So, both tasks b2 and b3 are constrained with SEQUENTIAL HANDLING b1. And b3 is not constrained with SEQUENTIAL HANDLING b2.

This is, perhaps, a subtle distinction. But consider this extended version of our earlier example:

WITH ( SERIAL )

{

SPAWN a0;

WITH ( PARALLEL )

{

SPAWN a1();

SPAWN a2();

}

SPAWN a3;

}



Which is equivalent to:

SPAWN a0();

SPAWN a1() WITH SERIAL a0;

SPAWN a2() WITH SERIAL a0;

SPAWN a3() WITH SERIAL a1, SERIAL a2;



Both Tasks a1 and a2 run concurrently, after a0 has completed. Task a3 runs sequentially, after both a1 and a2 have completed. And the enclosed WITH statement [WITH ( PARALLEL )] is treated as a set, as a singular entity.

Naturally, this nesting mechanism extends, through induction, to more complex situations. For example:

WITH ( SEQUENTIAL HANDLING PREVIOUS )

{

SPAWN c0;

WITH ( SEQUENTIAL EXPANSION PREVIOUS )

{

SPAWN c1();

WITH ( SEQUENTIAL EXECUTION PREVIOUS )

{

SPAWN c2();

SPAWN c3();

}

SPAWN c4();

}

SPAWN c5;

}



Is equivalent to:

SPAWN c0();



SPAWN c1() WITH SEQUENTIAL HANDLING c0;



SPAWN c2() WITH SEQUENTIAL HANDLING c0,

SEQUENTIAL EXPANSION c1;



SPAWN c3() WITH SEQUENTIAL HANDLING c0,

SEQUENTIAL EXPANSION c1,

SEQUENTIAL EXECUTION c2;



SPAWN c4() WITH SEQUENTIAL HANDLING c0,

SEQUENTIAL EXPANSION c2,

SEQUENTIAL EXPANSION c3;



SPAWN c5() WITH SEQUENTIAL HANDLING c1,

SEQUENTIAL HANDLING c2,

SEQUENTIAL HANDLING c3,

SEQUENTIAL HANDLING c4;



Which also illustrates how PREVIOUS, in a WITH construct, can simultaneously have multiple, different values depending upon the enclosure level that that PREVIOUS refers to.

7 Appendix A: TDL Keywords

TDL reserves all the standard C++ reserved keywords. TDL reserves all keywords beginning with _TDL. Inside TDL Tasks, TDL further reserves the following case-sensitive keywords:



_longjmp _setjmp catch finally
goto longjmp setjmp siglongjmp
sigsetjmp this throw


And TDL reserves the following case-insensitive keywords:



#using ACHIEVEMENT
ACTIVATE ACTIVATE_AT
ACTIVATE_IN ACTIVE
AFTER AT
COMMAND COMPLETED
DELAY DELAY_EXPANSION
DISABLE DISABLE_FOR
DISABLE_UNTIL ENABLED
EXCEPTION EXECUTION
EXPAND EXPAND_FIRST
EXPANSION EXTERN
FAIL FIRST
FOR getNumberOfActivates
getNumberOfTriggers GOAL
HANDLER HANDLES
HANDLING IN
INOUT MAXIMUM
MAXIMUM_ACTIVATE MAXIMUM_TRIGGER
MONITOR OUT
PARALLEL PARENT
PERIOD PLANNING
POSTPONE PREVIOUS
SELF SEQUENTIAL
SEQUENTIAL_ACHIEVEMENT SEQUENTIAL_EXECUTION
SEQUENTIAL_EXPANSION SEQUENTIAL_HANDLING
SEQUENTIAL_PLANNING SERIAL
SPAWN SUCCESS
TASK TDL_REF
TERMINATE TERMINATE_AT
TERMINATE_IN TRIGGER
UNTIL WAIT
WITH


Underscores ("_") in the above keywords will match zero or more actual underscores. Thus making TDLREF, TDL_REF, and TDL___REF all valid forms of TDL_REF.



EXTERN and FOR are only case-insensitive when used as part of TDL statements. When used as part of C/C++ statements, both revert to being case-sensitive.



TDL supports both the /*...*/ and the //... styles of commenting. The TDL compiler also supports Java-style /**...*/ documentation comments, which can be used to create HTML documentation.



TDL further supports the following case-sensitive C++ macros & typedefs:



_TDL_MARKUSED MARKUSED
TDL_ARG TDL_BaseException
TDL_FAIL TDL_FAIL_DATA_ARG
TDL_SUCCESS TDL_TASKDATA
TDL_TaskData TDL_TaskData_Ref


These typedefs and macros are defined in the tdl.H header file. They are primarily used in C++ routines handling POSTPONE'd Tasks as a means of manually interfacing with the TDL system.



The TDL header file stdtypes.H further extends this list with a collection of commonly used system-independent types, macros, and inlined functions.





8 Appendix B: Abbreviated TDL Syntax



Note: Text contained in Brackets ([]) denotes optional components. Brackets contained in quotes ("[", "]" ) represent brackets required by the TDL syntax. Text contained in Braces ({}) represents a required limited choice. One and only one of the items inside the Braces ({}) must be used. Parenthesis, where they occur, are required symbols in the TDL syntax.





TDL-File:

Task

any-valid-C++-statement-including-classes-methods-and-functions



Task:

EXTERN task-type task-name ( task-arguments ) ;

GOAL task-name ( task-arguments ) [simple-constraint] compound-statement

COMMAND task-name ( task-arguments ) [simple-constraint] compound-statement

MONITOR task-name ( task-arguments ) [monitor-constraint] compound-statement

EXCEPTION task-name ( task-arguments )

[ : base-exception-task-name ( base-exception-Arguments ) ] ;

[EXCEPTION] HANDLER task-name ( task-arguments ) [ handler-constraint , ]

HANDLES exception-task-name [ , handler-constraint ] compound-statement



task-type:

TASK

GOAL

COMMAND

MONITOR

EXCEPTION

[EXCEPTION] HANDLER



task-name:

identifier



task-arguments:

task-arguments , task-arguments

any-valid-C++-type identifier [ = C++-constant-expression ]



base-exception-arguments:

base-exception-arguments , base-exception-arguments

exception-task-argument's identifier

any-valid-C++-constant-expression



compound-statement:

{ statement-list }



statement-list:

statement

statement statement-list



statement:

[ tag : ] SPAWN task-name ( task-expression ) [ WITH constraint ] ;

[ tag : ] WITH ( constraint ) statement ;

task-name ( task-expression ) ;

task-identifier constraint;

SUCCESS ;

POSTPONE ;

FAIL exception-task-name ( task-arguments ) ;

compound-statement

Permitted-C++-statements (See Notes)



expression:

Permitted-C++-expressions (See Notes)

TDL_REF ( task-identifier )

TDL_REF ( PARENT )

TRIGGER() /* Only Inside Monitor Tasks */

getNumberOfTriggers() /* Only Inside Monitor Tasks */

getNumberOfActivates() /* Only Inside Monitor Tasks */



task-expression:

task-expression , task-expression

expression



tag:

identifier



task-identifier:

task-name

tag



simple-constraint:

simple-constraint , simple-constraint

EXPAND FIRST

DELAY EXPANSION

[EXCEPTION] HANDLER exception-handler-task-name ( task-arguments )



monitor-constraint:

monitor-constraint , monitor-constraint

simple-constraint

SEQUENTIAL EXPANSION

SEQUENTIAL EXECUTION

SEQUENTIAL PLANNING

SEQUENTIAL ACHIEVEMENT

MAXIMUM ACTIVATE C++-Integer-Constant

MAXIMUM TRIGGER C++-Integer-Constant

PERIOD relative-time



handler-constraint:

handler-constraint , handler-constraint

MAXIMUM ACTIVATE C++-Integer-Constant



constraint:

constraint , constraint

simple-constraint

SEQUENTIAL HANDLING [ reference-task ]

SEQUENTIAL EXPANSION [ reference-task ]

SEQUENTIAL EXECUTION [ reference-task ]

SEQUENTIAL PLANNING [ reference-task ]

SEQUENTIAL ACHIEVEMENT [ reference-task ]

SERIAL [ reference-task ]

PARALLEL

WAIT

DISABLE [ constraint-option ] UNTIL event

DISABLE [ constraint-option ] UNTIL absolute-time

DISABLE [ constraint-option ] FOR relative-time [ AFTER event ]

TERMINATE AT event

TERMINATE AT absolute-time

TERMINATE IN relative-time [ AFTER event ]

ACTIVATE AT event

ACTIVATE AT absolute-time

ACTIVATE IN relative-time [ AFTER event ]



reference-task:

PREVIOUS

PARENT

SELF

task-identifier [ array-index ]



constraint-option:

HANDLING

EXPANSION

EXECUTION

PLANNING /* Historical Alias for Expansion */

ACHIEVEMENT /* Historical Alias for Execution */



state-boundary:

ENABLED

ACTIVE

COMPLETED



event:

reference-task [ constraint-option ] state-boundary



absolute-time:

numeric-{0-23} : numeric-{0-59} [ : numeric-{0-59} [ . non-negative-numeric ] ]



relative-time:

[ [ non-negative-numeric : ] non-negative-numeric : ] non-negative-numeric [ . non-negative-numeric ]



Array-Index:

"[" "]"

"[" "]" Array-Index

"[" non-negative-numeric "]"

"[" non-negative-numeric "]" Array-Index





Notes:



When the SPAWN statement is utilized outside of Tasks, inside C++ functions or methods, any constraints on that SPAWN statement are limited to utilizing only the reference-task's PREVIOUS, PARENT, and SELF. Such tasks are always attached to the TCM_RootNode(), which forms their parent.



Tasks that are invoked as a function, without the SPAWN keyword, do not receive any constraints from enclosing WITH statements. They are run with the sole constraint of WAIT. And they are not considered when deciding the value of PREVIOUS.



The [EXCEPTION] HANDLER constraint may not be utilized inside a WITH Statement's constraint list.



TRIGGER(), getNumberOfTriggers(), and getNumberOfActivates() are expressions that may be used anywhere normal C++ expressions are used, including in the predicate of "if" and "switch"

statements. However, they are restricted to being used only inside Monitor Tasks.



TDL keywords are case-insensitive. Capitalization does not matter. "SERIAL", "serial", and

"SeRiAl" are all equivalent in TDL.



Underscores in TDL keywords wildcard-match zero or more actual underscores. DISABLEFOR, DISABLE_FOR, and DISABLE___FOR are all valid forms of DISABLE_FOR.



These C++ elements are outlawed inside TDL Tasks: "goto", "throw", "catch", "finally", "setjmp", "longjmp", "_setjmp", "_longjmp", "sigsetjmp", "siglongjmp", and "this".



Tasks may be referred to before they are SPAWN'ed. This is termed a future reference. Past references to Tasks that have already been SPAWN'ed are also permitted. However, in Constraint Statements, constraints that disable or delay the Task may not be effective on past references if that Task has already been started.



If a reference-task is not specified for a constraint, PREVIOUS is assumed.



Macros may not be defined inside TDL Tasks. However, macros may be defined outside TDL Tasks and invoked as functions or constants from inside TDL Tasks.



Utilization of a future-reference that is a WITH statement inside a constraint is unsupported in the current TDL compiler. TDLC will generate an error when it detects this situation.







9 Appendix C: Using the TDL Compiler



TDLC, the TDL Compiler, consists of Java and C++ components. The Java components form a translation program that translates TDL into C++ code. The C++ components form a library, libtdl.a, that is used by the generated C++ code to resolve dynamic binding issues. That library, libtdl.a, in turn is based upon the TCM library, libtcm.a.





9.1. Compiling the TDL Compiler



You will need:



Directions:



9.2. Using the TDL Compiler



Compile your TDL programs by saying "java TDLC -2Sy foo.tdl", where your program is located in the file foo.tdl. This will generate two files: foo.H and foo.C. Which can then be compiled by saying "g++ foo.C -o foo -ltdl -ltcm -I. -I${TDL_INCLUDE_DIR} -I${{TCM_INCLUDE_DIR} -L${TDL_LIBRARY_DIR} -L${TCM_LIBRARY_DIR}" into a ./foo executable for you to run.



If you are using csh, you may find these two aliases useful:



alias _tdlc_RUN_AND_ECHO_COMMAND 'echo \!:*:q ; \!:*'

alias tdlc '_tdlc_RUN_AND_ECHO_COMMAND java TDLC -2Sy \!:1 ; set _tdlc_STATUS=$status ; set _tdlc_COMPILE_COMMAND="g++ -g \!:1:r.C -o \!:1:r -ltdl -ltcm -I. -I${TDL_INCLUDE_DIR} -I${{TCM_INCLUDE_DIR} -L${TDL_LIBRARY_DIR} -L${TCM_LIBRARY_DIR}" ; if ( ${_tdlc_STATUS} == 0 ) echo ${_tdlc_COMPILE_COMMAND}; if ( ${_tdlc_STATUS} == 0 ) ${_tdlc_COMPILE_COMMAND}'





9.3. TDL Compiler Options



Saying "java TDLC -h" will provide you with a complete list of all the options to TDLC. While reviewing this list, you should remember that TDLC is experimental software. And as such it contains numerous options that will, in all probability, never be used again. That said, the important options are as follows:



-h Print help/usage information.

-V Print current version number.

-n Show what would happen without actually doing it.

-y Auto-answer "Yes" when asked about overwriting files.



-o <file> Specify the output filename.

-H Generate the Header file. Without the "-o" flag, this file is sent to stdout.

-C Generate the Code file. Without the "-o" flag, this file is sent to stdout.

-S Generate both the Header and Code files. These are named sourceFile.H and sourceFile.C based upon the sourceFile name. If the "-o" flag is specified, it overrides the sourceFile name. If the sourceFile name ends in ".tdl", the ".tdl" suffix is removed before appending ".H" or ".C".



-1 The generated class declarations are located in the generated header file. Generated task functions are inlined and also located in the generated header file. #using statements are translated to #include statements and located in the generated header file.

-2 The generated class declarations are located in the generated header file. Generated task functions are external, declared in the generated header file, and located in the generated code file. #using statements are translated to #include statements and located in the generated header file.

-3 The generated class declarations are located in the generated code file. The generated task functions are external, declared in the generated header file, and located in the generated code file. #using statements are translated to #include statements and located in the generated code file. The generated header file consists of nothing more than a set of generated external task function declarations.





10 Misc:

missing:



macros. (limitation)

Comments.

Document-comments (compiling to html documentation)



Figures.



Appendixes:



Examples.





Add table-of-contents

Add HTML links. (Resetting "See Section" numbers.)





Future references?



Extern tasks...

IN, OUT, INOUT...

Spawning tasks before they are declared. (Ie: That it's not a problem.)

Constraint Times from variables in MSecs...