This is covered in section 9.1 of the TDL manual. (Caveat: The TDL manual's links have become slightly outdated. The manual needs to be revised.)
You will need:
On systems that do not already have them, such as Solaris, you will also need:
This is covered in section 9.1 of the TDL manual.
For the purposes of this FAQ, we will assume that your copies of tcm, tdl, and ifc, are installed in /home/tcm, /home/tdl, and /home/ifc/ifc111 respectively. If this is not the case, make the appropriate corrections to these paths.
The following are specified in csh format. If you are using a bourne shell [sh, ksh, bash] syntax, replace setenv foo bar with export foo=bar.
You will also need to include java/javac and javacc in your path. Under csh, this can be done with:
On some systems, such as Solaris, you will also need to include gnu make and a bsd-style install program in your path.
This is covered in section 9.1 of the TDL manual.
For the purposes of this FAQ, we will assume that your copies of tcm and tdl are installed in /home/tcm and /home/tdl respectively. If this is not the case, make the appropriate corrections to these paths.
For g++ (linux):
For Solaris CC:
This is covered in section 9.1 of the TDL manual.
For the purposes of this FAQ, we will assume that your copies of tcm and tdl are installed in /home/tcm and /home/tdl respectively. We will further assume that, if you are using Solaris, your copy of bash is located in /usr/local/bin/bash. If this is not the case, make the appropriate corrections to these paths.
For g++ (linux):
For Solaris CC:
For each ${FILE}.tdl regression test-file, make qt generates ${FILE}.C and ${FILE}.H source files. It then compiles these C++ source files into a ./${FILE} executable, runs that executable, and stores the output and error streams in ${FILE}.out. As these output files tend to include hexadecimal addresses of objects in memory, and as these addresses change from run to run, ${FILE}.out is subsequently run through a sed script to filter out these memory addresses, with the output being stored in ${FILE}.out.sed. This final file is then diff'ed against a master file, ${FILE}.master_out.sed, which we have provided and previously verified as being correct.
make qt will report numerous warnings during the compilation of the regression tests. This is normal, intentional, and utilized internally to the development team to stress-test borderline coding practices.
Afterwards, make qt will print a summary page counting the number of lines that are different between the expected output (${FILE}.master_out.sed) and the generated output (${FILE}.out.sed). It will look something like this:
reid_1: 7 15 59 ******************** reid_2_test: 0 0 0 reid_3_test: 0 0 0 test_2_4: 0 0 0 test_2_5: 24 72 552 ******************** test_2_6: 0 0 0 test_2_6a: 0 0 0 test_2_7a: 0 0 0 test_2_8: 0 0 0 ....
The numbers indicate the output of diff ${FILE}.master_out.sed ${FILE}.out.sed | wc. 0's indicate that no differences were found. Larger numbers indicate the number of differences found. For convenience, any non-zero lines are flagged with asterisks.
Minor differences may occur here through realtime race conditions. This is normal. In the example above, taken from a Solaris CC run, we see this occurring:
% diff reid_1.master_out.sed reid_1.out.sed 5d4 < Doing A 21d19 < Doing B 59a58,59 > Doing A > Doing B
As we've chosen this example from a Solaris CC test run, we see another common minor difference occurring here: The Solaris CC iostream library displays NULL as 0, whereas Linux g++ iostreams library displays NULL as (nil). i.e.:
% diff test_2_5.master_out.sed test_2_5.out.sed 273c273 < ActualReferenceNode = (nil) ("NULL") --- > ActualReferenceNode = 0 ("NULL") 381c381 < ActualReferenceNode = (nil) ("NULL") --- > ActualReferenceNode = 0 ("NULL") 395c395 < ActualReferenceNode = (nil) ("NULL") --- > ActualReferenceNode = 0 ("NULL") 409c409 < ActualReferenceNode = (nil) ("NULL") --- > ActualReferenceNode = 0 ("NULL") 423c423 < ActualReferenceNode = (nil) ("NULL") --- > ActualReferenceNode = 0 ("NULL") 506c506 < ActualReferenceNode = (nil) ("NULL") --- > ActualReferenceNode = 0 ("NULL")
For your convenience, you can obtain a diff of all the regression test-files by saying "make diff".
This is covered in section 9.1 of the TDL manual.
For the purposes of this FAQ, we will assume that your copy of tdl is installed in /home/tdl. We will further assume that, if you are using Solaris, your copy of bash is located in /usr/local/bin/bash. If this is not the case, make the appropriate corrections to these paths.
For g++ (linux):
For Solaris CC:
This generates the /home/tdl/tdl_x.jar file.
This .jar file now contains the entire compiled TDLC (java) program software. It can be used in place of all TDLC's compiled java .class files in your CLASSPATH when running TDLC.
This .jar file is strictly a matter of convenience. The CLASSPATH's
".:/home/tdl/tdl_x.jar:/home/ifc/ifc111/classes"and
".:/home/tdl/PARSER:/home/tdl/DATA:/home/tdl/GRAPH:/home/tdl/GUI:/home/ifc/ifc111/classes"are equivalent. However, the former, utilizing a .jar file, is easier to type, less prone to errors, and may be somewhat faster for the java runtime environment to load (access).
This is covered in section 9.2 of the TDL manual.
The simplest way to get started compiling your TDL programs is by saying:
This will translate the TDL program located in foo.tdl into two C++ files: foo.H and foo.C. These two files can the be compiled with your C++ compiler.
For g++ (linux):
For Solaris CC:
We also recommend that you include these optional flags: (Note: These could affect the use of templates in your software. Please remember: They are optional.)
For g++ (linux):
For Solaris CC:
If you are using csh, you may find these two aliases useful:
They permit you to compile a foo.tdl file into a foo.H file, a foo.C file, and a ./foo executable file by merely saying "tdlc foo.tdl".
This is covered in section 9.3 of the TDL manual.
"java TDLC -h" will provide you with a full list of the TDLC options. However, many of these options are legacies of TDLC's more experimental days. The options you will find most useful are:
To Disable TCM messages, use:
TCM_SetFileLoggingOptions ( TCM_TerminalLoggingOptions() ); TCM_SetTerminalLoggingOptions ( Log_None );
To Disable TDL messages, use:
TDL::setLogStream ( "/dev/null" );
Though we usually just redirect TDL messages into a local file, so we can utilized them for debugging later on, should that prove necessary.
You may also want to experiment with:
TDL::setReportingLevel ( TDL::NO_REPORTING );
When a SPAWN occurs inside one or more iteration loops, that SPAWN statement may actually be invoked multiple times. One can refer to a specific individual instance of that SPAWN by specifying the iteration indexes, like this:
for ( i = 0; i < 10; i++ ) { for ( j = 0; j < 10; j++ ) { SPAWN foo(); } } SPAWN bar WITH SERIAL foo [ 7 ] [ 3 ];
Inside an iteration loop, one can refer to the current loop index with the special case "[.]", like this:
while ( test() == TRUE ) { SPAWN foo(); SPAWN bar() WITH SERIAL foo [ . ]; }
It is important to note that the array indexes specify the iteration loop index, not the Nth SPAWN invocation. Consider:
for ( i = 0; i < 10; i++ ) { if ( i % 2 == 0 ) SPAWN foo(); } SPAWN bar WITH SERIAL foo [ 7 ];
The "WITH SERIAL foo [ 7 ]" constraint here becomes a no-op, as foo() was not SPAWN'ed during the 7th loop iteration cycle.
Sometimes one has a TCM_Task_Tree_Ref variable that one wishes to use inside a TDL constraint. TDL_BIND exists for this purpose. TDL_BIND takes one or two arguments. Consider:
const TCM_Task_Tree_Ref & getTask(); Goal foo( TCM_Task_Tree_Ref theOtherTask ) { TDL_BIND ( theOtherTask ); TDL_BIND ( getTask(), aTask ); SPAWN b1() WITH SERIAL theOtherTask; SPAWN b2() WITH SERIAL aTask; }
In the single-argument case, the argument is taken to be both a TCM_Task_Tree_Ref variable containing the Task, and the TDL-reference-name for that Task.
In the two-argument case, the first argument is the name of a TCM_Task_Tree_Ref variable containing the Task, and the second argument is a text string by which this task will be referenced in TDL statements (constraints). The second argument may not be a variable. If it is a variable, it will not be dereferenced. Consider:
Goal foo( TCM_Task_Tree_Ref theOtherTask ) { const char * NAME = "foo"; TDL_BIND ( theOtherTask, NAME ); SPAWN b1() WITH SERIAL foo; // Error: This doesn't work. SPAWN b2() WITH SERIAL NAME; // Correct: This works. }
TDL_BIND does not support binding to array indexes. However, with TDLC Version1.2, TDL_BIND will support being overridden. Consider:
Goal foo( TCM_Task_Tree_Ref theOtherTask ) { for ( int i = 0; i < 10; i++ ) { TDL_BIND ( theOtherTask, TASK[i] ); // Error: This doesn't work. TDL_BIND ( theOtherTask, TASK ); // Correct: This works. SPAWN bar() WITH SERIAL TASK; // TASK value was overridden. } }
Sometimes one needs to access the underlying TCM data structures for a particular SPAWN'ed Task. In this situation, one should use TDL_REF as follows:
TCM_Task_Tree_Ref barTask; Goal foo() { SPAWN bar(); barTask = TDL_REF ( bar ); }
In the event that the SPAWN statement is inside an iteration loop, this becomes slightly more complex. TDL_REF returns a libtdl.a object that contains the set of all the SPAWN instances in that (those) iteration loop(s). It is necessary to further dereference this object to obtain a specific SPAWN statement. Consider:
TCM_Task_Tree_Ref barTask; Goal foo() { int4 i, j, k; for ( i=0; i < 10; i++ ) for ( j=0; j < 10; j++ ) for ( k=0; k < 10; k++ ) SPAWN bar(); i = 1; j = 2; k = 3; barTask = TDL_REF ( bar ) [ i ] [ j ] ( k ); /* Or */ barTask = TDL_REF ( bar ) . cacheDescendIntoBranch ( i ) . cacheDescendIntoBranch ( j ) . cacheDescendIntoNode ( k ); }
For user convenience, the operator[] method maps to the cacheDescendIntoBranch() method, and the operator() method maps to the cacheDescendIntoNode() method. It is important to note that only the last index uses "(k)" (cacheDescendIntoNode(k)), and that all the previous indexes use "[...]" (cacheDescendIntoBranch(...)).
TDLC includes a special construct, "#using", for this purpose. #using is analogous to #include, and is in fact translated to a #include statement during compilation (translation).
#using is "smart" in that it will recognize the ".tdl" suffix, and correct it to ".H" automatically. This permits one to write:
#using "foo.tdl" #using <bar.tdl> Goal foo() { // Code creating tasks defined in foo.tdl & bar.tdl }
And these #using statements will automatically be converted over to #include "foo.H" and #include <bar.tdl>, respectively, during compilation (translation).
Now TDLC is designed to generate both a Source (.C) file and a Header (.H) file. Translated #using statements (now #include statements) generally appear in only one of these files. Which one is determined by the flags to TDLC ("-1", "-2", "-3", or "-4").
This default behavior can be overridden with the "-t" (force translation of #using to #include) and "-N" (strip out all #using statements) flags to TDLC. (Run "java TDLC -h" for more information.)
Occasionally one needs to include type definitions from another C++ Header file. Typically this is done by saying #include "headerfile.H", a statement which TDLC then places in the generated Source (.C) file during compilation (translation).
However, a special case arises when the type information is used in Task arguments. In this case, the generated header file needs to know about the type information, as well as the generated source file.
In this situation, you should use the TDLC #using construct to include your header file:
#using "foo.H" #using <bar.H> Goal foo( fooType aFoo, barType aBar ) { // Code using aFoo & aBar }
Caveat: This will work properly with TDLC options "-1" & "-2". With TDLC options "-3" & "-4"), you will need to force the translation of #using in the header file with the "-t" flag (i.e. -4Ht), and, if you don't want the file included a second time in the source file, disable the translation of #using in the source file with the "-N" flag (i.e. -4CN).
TDL Tasks can accept any valid C++ data type, including pointers to functions, class instances, etc. However, these data types must be available in both the generated Source (.C) and Header (.H) files. At present, the only way to do this is to include the files defining your data structures with #using statements. (See Include type definitions from another file. and Use task's defined in another .tdl file..)
Terminating a Monitor Task from outside that Monitor Task is a simple matter of saying:
aMonitorTask TERMINATE;
However, terminating a Monitor Task from inside that Monitor Task is slightly more complex. As mentioned in Accessing Monitors, Monitors are actually a collection of Tasks, and saying:
THIS TERMINATE;
Merely kills the current instance of this Monitor, and does not prevent the monitor from creating additional instances later on. If you really do want to terminate the whole monitor from inside an instance of that monitor, you need to say:
TDL_BIND ( TCM_Parent(TDL_REF(THIS)), realMonitor );
realMonitor TERMINATE;
HOWEVER it is rare that one actually wants to terminate a Monitor Task as TDL & TCM provide an alternate, superior scheme. One can specify a maximum number of triggers for a Monitor, and then that Monitor will stop after it has been triggered the specified number of times. i.e.:
Monitor aMonitorTask ( ) WITH MAXIMUM TRIGGER 1 { // Code; if ( timeToStopMonitor ) { trigger(); } }
Of course, one could specify a maximum number of triggers larger than 1, and test for the number of trigger()'s encountered so far with the getNumberOfTriggers() function. (You may also wish to test for the number of activates so far with the getNumberOfActivates() function.) Both of these functions are only accessible inside Monitor Tasks.
TDL Tasks, when compiled (translated) with TDLC, have a series of functions created for them that provide a basic interface to allocating, creating, and possibly resuming that task. Extern tasks declare these functions as extern, and do nothing else.
Extern Tasks were originally intended to be used to interface to legacy TCM code. There is no reason why you should ever need to create an Extern Task. All necessary pre-declarations (forward references) are automatically taken care of for you in the headers. And #using will allow you to include tasks from that are defined in other files.
One word of caution: With TDLC, declaring a task twice is considered an error, regardless of whether one, both, or neither definitions are extern.
Most constraints are Conjunctive: That is, if you apply two constraints to the start of a Task, both constraints must be resolved before the Task can start.
Terminate and Activate constraints are Disjunctive: That is, if you apply two Terminate constraints to a Task, the task will be terminated when either constraint is satisfied.
A few specific constraints can not be combined. MAXIMUM ACTIVATE, MAXIMUM TRIGGER, and MONITOR PERIOD specify limitations for which there can only be one value. In this situation, the last constraint applied is considered to be the binding (final) value.
Tasks can utilize the PERSISTENT Task-level constraint. These constraints consist of the keyword PERSISTENT followed by a variable declaration. i.e.:
Goal foo() WITH PERSISTENT double bar, PERSISTENT u_int4 cee = 12, PERSISTENT int ((*f)(void *))(double *, float *) = theFunction { // Code using variable bar goes here. }
These PERSISTENT variables are then usable throughout the Task, and any additional RESUME Task, if one is also present. PERSISTENT variables are NOT copied onto the stack. Instead, they are located in memory that is automatically dynamically allocated off the heap inside the TDL/TCM internal libraries.
These internal libraries will automatically deallocate this memory when it is no longer needed through a reference-counting scheme. You should NOT attempt to free() this internal data. (Though, of course, if you allocate memory yourself with malloc() or new, you are responsible for free()'ing that memory.)
As PERSISTENT variables are not copied onto the stack, Monitor instances will all share the same data. Consider:
Monitor foo() WITH PERSISTENT int4 count = 1, PERIOD 0:0:1.0, MAXIMUM ACTIVATE 3 { cout << "Count = " << (count++) << endl; }
Which generates:
Count = 1 Count = 2 Count = 3
Occasionally, particularly when interfacing with other systems, one needs to postpone completion of a Task until some event transpires. Now, spin-waiting inside the task can become problematic, particularly when other Tasks need to run in the meantime. For this reason, one can postpone completion of a Task via the POSTPONE statement. Under these circumstances, the Task's code stops executing as if a RETURN statement had been executed. However, internally, the Task is marked as being uncompleted, and the Task scheduling code waits to be informed that Task has been completed. Of course, while this POSTPONE'd task is pending, other, independent tasks are free to run.
Resuming a POSTPONE'd Task can be done by directly manipulating the underlying data structures through TCM. However, a more convenient mechanism exists. One can declare a Resume Task with the same name as the POSTPONE'd Task, and then invoke that Resume Task with TDL_RESUME_nameOfTask. The first argument to TDL_RESUME_nameOfTask needs to be the instance of the POSTPONE'd Task that is being resumed. After that come the arguments specified in the Resume Task. i.e.:
Goal foo ( int goalArg ) { /* ResumeFoo will run while we are POSTPONE'd. */ SPAWN ResumeFoo ( TDL_REF ( THIS ) ); POSTPONE; } Resume foo ( int resumeArg ) { // Resume Code goes here. } Goal ResumeFoo( TCM_Task_Tree_Ref theFooToResume ) { TDL_RESUME_foo ( theFooToResume, 2 /*resumeArg*/ ); }
You will note that foo establishes the means for its own resumption. This is not essential, but it is important that foo be resumed after it has started running.
You can employ POSTPONE inside a Resume Task. In this situation, the Task is considered to be still POSTPONE'd, and the Resume Task will need to be invoked again. (So if you wanted to test a value, and only complete if that test proved positive, you can do so.)
Monitors form an unusual task in that they are actually a collection of Tasks. The primary monitor task is created internally to TCM. It in turn creates children, or instance tasks, whenever it is activated, be that through explicit ACTIVATE constraints or periodically. Each of these children actually runs the code defined in the TDL Monitor Task. Complicating matters, they all share parts of the same data structure, allowing them to share "PERSISTENT" declarations. (Though, of course, they all have independent copies of their own arguments on their local stack frame.)
Normally, this is not a major issue. However, when attempting to perform actions upon a monitor from inside a monitor, it becomes important to realize that there's an extra layer in the TCM task-tree hierarchy.
Now, some functions will recognize when you are inside a Monitor-instance and deal with it appropriately. Others will require you to deliberately and explicitly reference the main Monitor Task. For an example, see Terminate a MONITOR Task.
There is an ongoing problem wherein a Task that is spawned with the WAIT constraint can suffer the "Nesting" bug. What happens is that, while you are WAIT'ing on your Task, other Tasks can be SPAWN'ed. And, unfortunately, because the implementation of WAIT, you are forced to wait until those other Tasks have finished too.
Now, if those other Tasks were all SPAWN'ed as children of the Task you are WAIT'ing on, there's no problem. In this situation, you are already WAIT'ing on these children Tasks. But, if you have multiple independent, concurrent, perhaps even periodic Tasks, you can wind up waiting for a very long time on Tasks you never intended to WAIT on.
This problem is compounded by the fact that you can invoke a Task as a function by leaving out the SPAWN and WITH keywords, and in doing so run the Task with the sole constraint of WAIT.
When TDLC compiles (translates) a .tdl file into the corresponding Source (.C) and Header (.H) files, certain pieces of data from the original .tdl file are replicated. Naturally, TDLC makes use of #line macros to credit this information to the original source (.tdl) file for purposes of debugging.
However, in certain isolated, degenerate situations these #line macros can cause difficulties. Specifically, in certain situations information from the original source file is replicated in multiple locations. Sometimes, an error can occur during C++ compilation in one of these locations, but not in others. Thereby causing great confusion to our end users.
Our recommendation is that, if you think this is happening, try running TDLC with the "-L" option. This will disable #line macros and allow you to track the problem down more precisely. The TDLC generated Source (.C) and Header (.H) files have been designed for human-readability for just this purpose.
It has also been our experience that these problems most often arise when type information is defined in the .tdl file, or in a #include'd file, and then used in a Task's arguments. In this particular scenario, that type information is then unavailable in the generated Header (.H) file, and thus creates the error when certain declarations, which are automatically created, attempt to use it. (See Pass a structure/class/typedef argument into a Task.)
There's a little known bug in TDL 1.2 relating to changing a Monitor's period. It seems that if a Monitor starts running without a period it's considered to be a non-periodic Monitor. If you subsequently set the period... Well, I wrote the code and I don't think that's going to work quite right.
So my suggestion is that, if you plan to use the PERIOD constraint to change a Monitor's period to another time period, that you consider initially creating that monitor with a PERIOD constraint. This PERIOD constraint can be at the task-level on that Monitor (i.e. Monitor aMonitor() WITH PERIOD foo { ... }), on the SPAWN line (i.e. SPAWN aMonitor() WITH PERIOD foo;), or via an enclosing WITH statement constraint (i.e. WITH ( PERIOD foo() ) { SPAWN aMonitor(); }).
Consider:
for ( int4 i=0; i<10; i++ ) { for ( int4 j=0; j<10; j++ ) { WITH ( SERIAL ) { SPAWN foo(i, j); } } }
What effect does that WITH ( SERIAL ) statement have? One might, at first, be tempted to think that it serializes all those 100 foo Tasks that are about to be SPAWN'ed. But the WITH statement only applies its constraints to the statements inside of it. So what we actually have here is 100 WITH statements, each containing a single solitary SPAWN, and the SERIAL constraint becomes a no-op.
Consider:
void foo() //Or Goal foo() { int4 i = 2; SPAWN bar ( & i ); }
With standard C++, this is valid because bar finishes executing before foo() ends. In TDL, this is not necessarily true. (In the current single-threaded architecture, this is definitely not true.)
So what winds up happening here is that bar, when it starts running, has a pointer for an int4 that points onto the stack. Only that area of the stack has been deallocated, and perhaps reallocated for other purposes... So it may, or, more likely, may not have a value of 2. And if bar writes to i, it can corrupt some other routine's data.
This is called a Referential Transparency. It can occur in C or C++ too. Consider:
int * foo() { int4 i = 2; return & i; }
We recommend that you avoid making this mistake.
Last Updated $Date: 2000/07/05 23:09:11 $
Send questions or comments to
da0g+@cs.cmu.edu