This chapter shows you how to write programs for the ADAM environment, and how to compile, link, and test them. It starts off with an ultra-simple ‘Hello, world’ program, and then takes you through other examples, explaining what is going on. Later sections consider error handling, and how to combine several programs together into a composite program called a Monolith.
A comprehensive description of how to write ADAM programs is given in SUN/101.
Let’s write, compile, link, and run an ADAM program which writes ‘Hello world’ on your terminal. Here’s the program:
Store this in a file called HELLO.FOR.
You also need an interface file which, in its simplest form, is:
Store this is a file called HELLO.IFL.
Now, prepare the environment for ADAM program development:
and compile the program:
and run it:
You can also run the program from the ICL command language:
You have now prepared, compiled, linked and run your first ADAM program.
The source code of every ADAM program which is meant to last should be written in the style recommended by Starlink; in particular, it should contain a section giving information about the function of the program and the meaning of the parameters. This style is encapsulated in a set of Prologues, described in section 12.2. Once you start serious ADAM programming, you should base your code on these prologues. Their purpose is to ensure that your code is adequately documented and, in particular, can be supported easily by someone other than yourself.
The simple example programs in this chapter use a much simpler format than that recommended for normal ADAM programs as their purpose is pedagogic. They are usually presented first without comments in order to make their structure as clear as possible. Then, the code is explained line by line.
ADAM programs are written as Fortran subroutines with one argument — a status value. They should obey ADAM conventions; in particular, all communication with the user must be done via the parameter, message, or error systems, and the data system should be used to manipulate data. You must not use READ or WRITE statements to communicate with the user’s terminal directly. This is because a direct WRITE would bypass ADAM’s system of output control (which may be using a screen management system like SMS), and a direct READ would bypass the parameter system for obtaining parameter values. Furthermore, the sub-process in which the program is run may not be connected to a terminal.
An example of the source code for a simple program to read a real value from a terminal and write it out is:
In these teaching programs I use a horizontal line of dots to separate the declarations from the executable statements. This is not standard ADAM practice, but I think it makes the structure of the code easier to comprehend.
You can tell this is an ADAM program because it calls routines which implement the ADAM parameter and message systems. Let’s go through it line by line and see what is going on.
As already mentioned, the program is written as a Fortran subroutine with a single parameter called STATUS; the name of the subroutine is TESTR.
This makes the compiler tell us if we are using a variable which hasn’t been declared. It is a useful way of trapping spelling mistakes and declaration omissions. It is good programming practice to declare explicitly every variable used.
Before you can compile an ADAM program, you need to execute a command called ADAMDEV. Amongst other things, this defines a logical name called SAE_PAR to be the name of a file which contains the definitions of Fortran global constants which are needed in ADAM programs. You should always include this INCLUDE statement in your programs1.
These statements declare the types of the two variables, STATUS and XVALUE, used in the program.
This is the first executable statement, and is a call to one of the routines which implement the parameter system — you can tell this because its name starts with the characters ‘PAR_’. The rest of the name tells you its function: ‘GET’ means ‘get the value of a parameter’; ‘0’ means ‘the parameter has 0 dimensions’ (i.e. it is a scalar); ‘R’ means ‘present the parameter value to the program as a REAL number’. There is an extensive repertoire of similar routines with names like PAR_GET1I and PAR_PUTNR whose meanings can be broken down in a similar way. In fact you can specify the dimensionality of the parameter value to be:
and you can specify the type to be:
The PAR routines are described in APN/6 and summarised in section 21.1.
But what do the subroutine arguments stand for?
Well, the first argument ‘X’ is a CHARACTER expression specifying the name of the parameter whose value is being obtained. The next argument ‘XVALUE’ is the name of the REAL scalar variable which is to hold the value obtained for the parameter. The final argument ‘STATUS’ is an INTEGER variable which will hold the value of the Status returned by the routine.
To sum up: this statement obtains the value of parameter X from the ADAM parameter system, stores it as a REAL number in scalar variable XVALUE, and stores the returned status value in variable STATUS.
Now we test the status value returned by PAR_GET0R by comparing it with the constant SAI__OK. This is one of those constants defined as a result of that ‘INCLUDE ’SAE_PAR” statement we came across earlier. SAI__OK means that ‘no error has been detected’, so if this test is satisfied we can execute the next two statements which cause the value read in to be displayed on the user’s terminal:
These two routines belong to the ADAM message system, which is the preferred way of displaying messages on the user’s terminal. Once again, the first four characters ‘MSG_’ show that they are message system routines, and the rest of the name indicates their function. The values of variables are passed to the message system by means of ‘tokens’, so the first thing to do is to set the value of a token. This is done by MSG_SETR which encodes the value of the REAL variable XVALUE and associates it with the token specified by the CHARACTER expression ‘X’. There is a different routine for each type of variable (MSG_SETL, MSG_SETI etc.).
N.B. The argument ’X’ used in PAR_GET0R and MSG_SETR give names to different things. In the case of PAR_GET0R, it is the name of a program parameter. In the case of MSG_SETR, it is the name of a message system token. The program doesn’t get confused because each routine interprets ’X’ in its own way.
The MSG_OUT routine constructs the message and writes it on the user’s terminal. ’MESS’ is a CHARACTER expression specifying the name of the message in the message system; ’Value from TESTR program is ^X’ is the message itself, where ^ indicates the token which will be replaced by the value set by MSG_SETR; and STATUS holds the status value returned by the routine. The MSG routines are described in Chapter 16.
terminate the IF statement and indicate the end of the program source code.
This program should be stored in a file called TESTR.FOR, ready for compiling. However, before the program can be run successfully, we need to prepare an interface file.
The interface file contains information on a program’s parameters and messages, and shields the program from details of the run-time environment which may not be known when the program is written — in particular, a program can ask for a parameter value without knowing how it will be obtained. The interface file should be called program.IFL where program is the name of the program. Notice that the interface file can be changed without having to change and recompile the program with which it is associated — useful flexibility. An example interface file for the TESTR program above is:
The format, content, and meaning of interface files are described in detail in Chapter 14. However, it should be clear that they begin and end with ‘interface’ and ‘endinterface’ statements respectively. They give information about parameters (parameter ... endparameter) and messages (message ... endmessage).
The example parameter specification (beginning ‘parameter X’) contains details about the TESTR program’s single parameter (named X) and how it should be treated. It does this by giving values for a number of fields in the format (field-name value). The field specifications shown above have the following effect:
The data type of the parameter value is _REAL. (This is one of the primitive object types in the HDS data system.)
A value for X may be given in the first parameter position on the command line.
The prompt-string that is to be presented to the user when the system asks for a value is ‘x value’.
‘ppath’ is short for ‘prompt-value-resolution-path’, and the purpose of this field is to specify where the suggested value offered to the user in the prompt is to come from. In this case, the current (last used) value will be used or, if there is no current value, the default value specified in the interface file will be used.
The default value is 1.5. Notice that, because of the ‘ppath’ specification above, this value will only appear in the prompt as the suggested value if there is no current value.
‘vpath’ is short for ‘value-resolution-path’, and the purpose of this field is to specify the search path the system is to follow when it is trying to obtain a value for a parameter. If a value is specified on the command line, the problem is solved and ‘vpath’ is not considered. However, if no value is specified, ‘vpath’ gives the system an ordered list of alternative sources to try. In the example above, only one source is specified: prompt the user to specify a value. Be careful to distinguish between the meanings of ‘vpath’ and ‘ppath’; ‘ppath’ is concerned with the prompt — hence the ‘p’, while ‘vpath’ is concerned with the value — hence the ‘v’.
The prompt will be of the form:
keyword - prompt-string /suggested-value/ >
In the example above, the keyword is taken as the ‘X’ in the ‘parameter’ statement, the prompt-string is specified by the ‘prompt’ statement, and the suggested-value is determined by the ‘ppath’ statement. Thus, if there is no current value, the prompt for parameter X would be:
By changing the specification of ‘vpath’ and/or ‘ppath’ in the interface file, the program can be made to accept a value obtained from a variety of sources.
The message specification:
tells ADAM that when the program outputs the message with the name ‘MESS’, the message ‘TESTR prints x’ (where x is the value associated with the message token ^X by the program) is to be displayed in preference to the message given in the program (i.e. in the source code). If the message specification is omitted from the interface file, the text given in the source code would be displayed (i.e. ’Value from TESTR program is x’).
Now that we have the source code stored in file TESTR.FOR and the interface module stored in file TESTR.IFL, we are ready to compile, link, and test our program TESTR.
The ADAM linking process links the subroutine with any other user-supplied or ADAM routines which are called, together with a fixed part which handles program startup, shutdown etc.
Before compiling and linking an ADAM program, the following commands must first be executed to set up the required logical names and symbols:
(You may already have executed the ‘ADAMSTART’ command, in which case you don’t need to execute it again.) You compile the program in the normal way:
producing the object code in file TESTR.OBJ. This can now be linked with the ADAM environment by:
to produce the executable file TESTR.EXE. The ALINK command is defined during the execution of ADAMDEV.
The TESTR program can now be tested using the command language ICL. First, start up ICL as shown in section 5.2, then a possible test session is as follows:
Let us consider this test session one command at a time. First, it is necessary to define a command to run the program — this is done by the command:
Here, ‘demo’ is the name of the command being defined, and ‘testr’ is the program to be run when the command is issued. For this to work, program TESTR has to be stored in one of the directories in the ADAM_EXE searchlist (such as your default directory). If it is stored somewhere else, a directory specification must be specified in front of ‘testr’ in the ‘define’ command. The next command:
causes our program TESTR to be loaded into subprocess xxxxTEST (as shown in the second line) and executed. As the parameter value (5.1) is provided on the command line, the interface file ‘vpath’ specification for parameter X is not used, i.e. the user is not prompted for a value. On execution, the program displays the message:
This was obtained from the specification for message MESS in the interface file, and not from the message stored in the MSG_OUT call in the program. Now try:
Notice that TESTR does not require re-loading (there is no loading message). As a value for X was not specified on the command line, the system displays a prompt in response to the ‘vpath ’prompt’’ field specification in the interface file. The suggested-value (5.1) is the current (last used) value rather than the default (1.5) value because of the order specified in the ‘ppath ’current,default’’ field specification in the interface file. The new value (4) is accepted and output in the message on the following line. Testing from DCL: It is possible to run the program directly from DCL by:
with an interface file like:
Similarly, we could write programs TESTL and TESTC to read and write logical and character type parameters. These can be used to explore the response of the parameter system and ICL command processor to different types of input value. These TESTx programs can also be combined into a single program called a monolith — this is demonstrated later in Section 11.8.
All but the simplest ADAM programs should be structured as a top level routine which calls one or more subroutines which, in turn, may call further routines. For example, consider this abbreviated sketch of the subroutine structure of a program to add two images together:
Being an ADAM program, it calls routines (like CMP_MAPV, DAT_ASSOC, DAT_NEW) in the ADAM libraries. Such routines have an integer parameter called STATUS and if they fail for some reason, STATUS will be set to an error code indicating the nature of the error, otherwise its value will remain unchanged. Our private routines (such as GETINP, CREOUT, ADDARR) should also have a STATUS parameter which should be set to an error code if an error is detected.
The treatment of the STATUS parameter is governed by the ADAM Error Strategy. This is:
The application of this strategy can be illustrated once again by the following program:
For each subroutine, the IF statement is the first executable statement. This error strategy means that it is usually not necessary to check STATUS after each routine is called. A series of routines can be called with STATUS being passed from one to the next. If an error occurs in one of them, the subsequent routines will do nothing and the final STATUS will indicate the error code from the routine that failed. If this value is then returned by the main routine to the fixed part, an error message will result. Thus, the error will be correctly processed with no special code being added to check for errors.
A program will always be executed by the ADAM system with an initial STATUS value of SAI__OK. This is because during the linking process it is linked with a ‘fixed part’ which calls it as a subroutine after having initialised STATUS to this value. Thus, it would appear to be unnecessary to test STATUS on entry to our program. However, sometime in the future we may want to call our program as a subroutine in another program and it might be called after a previous routine has set STATUS to an error code. Thus, in general, we cannot be sure what the value of STATUS on entry to our program will be, and we should always adopt the Starlink error strategy, even for our top-level routines.
This error strategy is illustrated by our next example program which calculates the square of the value of the input parameter ‘VALUE’, and writes it out as part of a message:
Here, STATUS is used in two tests. Firstly:
to implement the ADAM Error Strategy, and secondly:
to ensure that if the STATUS returned from PAR_GET0R is bad, the rest of the routine is not executed with an undefined value of R. Actually, it is not necessary to include MSG_OUT in the IF block as this routine would return immediately if STATUS was bad.
An example interface file for this program is:
Notice that the first parameter of the MSG_OUT routine is specified as a single blank character. This means that the message being output does not have a name and there is no specification for it in the interface file. This is the simplest method of using the message system, but it means that we cannot alter the message by specifying it in the interface file.
To test this example, enter the source code and interface file into files SQUARE.FOR and SQUARE.IFL, respectively; compile and link as for TEST, then use the following commands within ICL:
More sophisticated error handling can be provided by using routines in the ERR library. These facilities are described in Chapter 16.
We have seen how routines like PAR_GET0R get parameter values from the environment. It is also possible for programs to return values to the environment. The following modified fragment of program SQUARE does not output its result on the terminal (by using MSG_OUT), but returns it to the parameter VALUE using a call to the routine PAR_PUT0R, which is analogous to PAR_GET0R. Replace the second IF statement by:
We could run the modified program from ICL as follows:
In order for the program to return a value to ICL, we must use a variable for the parameter and place it on the command line:
The variable name must be placed in parentheses; the name of a temporary data object holding the value of the variable is used as the parameter by ICL.
A modification of this scheme is needed with character variables to allow for the case where the value of the character variable is itself a device, file or object name. In such cases, the supplied name cannot be replaced by some other name so, to indicate that they may not be replaced, name values in variables must be preceded by ‘@’. For example, in
the character string ‘devdataset’ would be stored in a temporary data object and the effect of ‘TRACE (X)’ would be to trace the temporary object and not devdataset. However, in
devdataset itself will be traced.
We have shown how to write ADAM programs called TESTR, TESTI, TESTL, and TESTC to read and display parameter values of four different types. It would be convenient to combine these four similar programs in a single program which would recognise the individual commands which call them. This can be done by combining them into a monolith. KAPPA is an example of a monolith in which a lot of small programs have been combined together. The advantage is that once the monolith has been loaded, all the component programs can be used without any further loading operations being necessary, and it only occupies one place in the task cache.
Let us produce a monolith called TEST which will contain the four TESTx programs mentioned above. The first thing to do is to create an object library to hold the object modules for the TESTx programs:
This will create an object library called REDUCE.OLB. Assuming the four TESTx programs have been compiled, we can store their object modules in this library by:
Now, write a program to call the TESTx programs in response to appropriate commands:
Store this in file TEST.FOR, then compile it:
and link it with the routines it calls by using the MLINK command:
(we must have executed the ADAMDEV statement in order to define the symbol MLINK). We now have the monolith stored in file TEST.EXE, but we also need an interface file stored in file TEST.IFL. This is simply a concatenation of the interface files for the TESTx programs enclosed in ‘monolith’ and ‘endmonolith’ statements:
The last thing to do before TEST is ready for use is to store definitions for the commands TESTC, TESTI, TESTL, TESTR in an ICL command file TEST.ICL:
Now, we are ready to use our monolith TEST. Start up ICL in the usual way, then execute the commands which define the commands to run the programs by loading the command file TEST.ICL:
Now, if any of the defined commands ‘testc’, ‘testi’, ‘testl’, ‘testr’ is entered, the monolith ‘test’ will be loaded:
Notice that the monolith TEST is only loaded once and that the command ‘testr’ is available for immediate use. The extra delay in loading the larger monolith and in defining the set of commands is made up for later by the faster response to subsequent commands.
1These last two statements (IMPLICIT and INCLUDE) can be replaced by the single statement INCLUDE ’SAI_PAR’. However, it is clearer if they are kept separate when explaining what is going on.