.CH "Ratfor Programming Under the Subsystem" This chapter describes the use of Ratfor in the programming environment provided by the Software Tools Subsystem. In addition to demonstrating use of the Ratfor preprocessor, Fortran compiler, and linking loader, the programming conventions necessary for the use of the Subsystem support subprograms are described. .pp In this chapter, a number of programming conventions are presented. Since very few of the conventions can be enforced by the Subsystem, adherence to these conventions must be left to up to the programmer. Many conventions, such as those dealing with indentation and comment placement, are shown because they assist in producing readable, maintainable programs. Violation of these conventions, while not critical, may result in unmaintainable programs and extended debugging times. Other conventions, such as those dealing with character string representations and input/output, are crucial to the proper operation of the Subsystem and its support subprograms. .ul Violation of these conventions can and will cause undesirable results. .MH "Requirements for Ratfor Programs" The Software Tools Subsystem is not an operating system. Rather, it is a collection of [ul cooperating] user programs. To run successfully under the Subsystem, a program [ul must] cooperate with it. Several things are required of Subsystem programs: .in +5 .ta 6 .tc \ .sp .ti -5 -\The program must terminate with a [bf stop] statement, or a call to the routine "error". The program [ul must not] "call exit" or invoke any of the Primos error reporting subroutines with the the "immediate return" key. A program's failure to terminate properly will also cause the Subsystem command interpreter to be terminated, leaving the user face-to-face with Primos. .sp .ti -5 -\The program should not have initialized common blocks (i.e. [bf block data).] Initialize the common areas with executable statements. (To link a program that must have initialized common, see appendix b.) .sp .ti -5 -\Local variables in a subprogram are placed on the stack unless they appear in a [bf data] or [bf save] declaration. The value of variables not appearing in one of these declarations is not defined on entry to a subprogram. .in -5 .pp Several conventions apply to the file containing the Ratfor source statements: .in +5 .sp .ti -5 -\The file name should end with the suffix ".r". .sp .ti -5 -\Any number of program units (main program, functions, and subroutines) may be included in the file, but the main program must be first. .sp .ti -5 -\All variables and functions must be declared in type statements (the Primos Fortran compiler enforces this restriction, except in the case of function names). .sp .ti -5 -\Each program unit must end with an [bf end] statement. .sp .ti -5 -\Since [bf defines] apply globally to all subsequent program units, a main program and all of its associated subprograms can be contained in the same file. Only one copy of definitions need be included at the beginning of the source file. .in -5 .MH "Running Ratfor Programs Under the Subsystem" Three steps are required to obtain an executable program from Ratfor source statements. The first step, preprocessing, produces ANSI Fortran statements from the Ratfor source statements. The second step, compilation, results in a relocatable binary module, which lacks all of the Primos, Fortran and Subsystem subroutines. The last step, linking, produces an executable object program by linking the relocatable binary module with the Primos, Fortran and Subsystem support routines necessary for its execution. The object program produced during linking may then be executed. .SH "Preprocessing" In the preprocessing step, the Ratfor preprocessor, 'rp,' is used to translate Ratfor source statements into semantically equivalent ANSI Fortran statements acceptable to the Primos Fortran compiler. The Ratfor preprocessor is invoked with a command line of the following syntax: .be rp [-o ] [] .ee .pp If you do not want a conventionally named output file, you may specify the option "-o ", where is the name you want given to the Fortran output. If you do not include a "-o " option, 'rp' will name the output file by appending ".f" to the name of the first . If the name of the first ends in ".r", the ".r" will be replaced by the ".f". .pp Next comes a list of the files containing Ratfor source statements to be preprocessed. 'Rp' reads the files in the order specified on the command line and treats the contents as if they were together in one big file. This means that [bf defines] in each input file apply to all subsequent input files. .pp Finally, there are preprocessor options which may be specified to change the output in some way or affect preprocessor operation. For a complete list of available options and a more detailed description of the command line syntax, see Appendix F. .pp In spite of all this complicated stuff, the 'rp' preprocessor is quite easy to use if you follow the recommended naming conventions for files. For instance, if you have a Ratfor program in a file called "prog.r", you can have it preprocessed by just typing .be rp prog.r .ee This command will cause the program contained in "prog.r" to be preprocessed, and the Fortran output to be produced on the file "prog.f" (which is exactly what the Fortran compiler expects). .pp Here are some more examples to show other ways in which 'rp' can be called: .be .ne 17 # preprocess the files "p1.r", "p2.r", and "p3.r" # and produce Fortran output on "p1.f" rp p1.r p2.r p3.r # preprocess the files "p1.r", "p2.r", and "p3.r" # and produce Fortran output on "ftn_out" rp p1.r p2.r p3.r -o ftn_out # preprocess the file "p1.r", produce the Fortran # on "ftn_out" and include code to produce # subprogram level trace rp -t p1.r -o ftn_out .ee .SH "Compiling" After turning your Ratfor source code into Fortran with the preprocessor, the next step is to compile the Fortran code. Since the Subsystem uses the Primos Fortran compiler, the 'fc' command just produces a sequence of Primos commands to cause the compilation. The following command will call the Fortran compiler for a compilation: .be fc [] [-b []] [-l []] .ee The Fortran source code must be in the file . The relocatable binary output will be placed in the file , unless "-b " is omitted. Then, following Subsystem conventions, the binary file name is constructed by appending the input file name with ".b"; if the input file ends with ".f", the "f" will be replaced by the "b". Normally no listing is produced; however, if one is requested, it will appear on the file , or if the listing file name is omitted, the name will be constructed by appending the ".l" to the input file name; again, if the input file name ends in ".f", the "f" will be replaced with the "l". .pp is a series of single letter options that specify how the compiler is to generate the object code. Since there are too many options to completely describe here, we will only mention a few of the more important ones. For those who wish to make full use of the Fortran compiler, or for those just curious, the .ul Software Tools Subsystem Reference Manual, or the 'help' command will give complete information. .pp Here are brief descriptions of the options of interest: .be .fi .in +5 .ta 11 .tc \ .ti -10 -v\Generate pseudo-assembly code describing the object code produced. .sp .ti -10 -i\Unless otherwise specified, consider all integers to be "long" (32-bit) rather than "short" (16-bit). (This is useful for programs ported from machines with longer word lengths.) .sp .ti -10 -t\Insert code to produce a statement-level trace during execution. .nf .in -5 .ee Of course, more than one of these options may be specified. .pp Again, even though all of this looks very complicated, it is really very simple, if you have used the Subsystem file naming conventions. If you have your Fortran code in a file named "prog.f" (remember where Ratfor put its output), you may compile it, using the default options, by just entering .be fc prog.f .ee The command will call the Fortran compiler to produce binary output in the file "prog.b". Just for completeness, here are some other examples of 'fc' commands: .be .ne 17 # Compile "p1.f" to produce the binary "p1.b" and # and a listing on "p1.l" fc p1.f -l # Compile "p1.f" to produce the binary "bin" and # the listing on "list" fc p1.f -b bin -l list # Compile "p3.f", produce a pseudo-assembly code # listing and default to 32-bit integers fc -v -i p3.f -l .ee .pp One problem you may encounter when using 'fc' is that the Primos Fortran compiler pays no attention to i/o redirection when it is writing error messages to the terminal. This is a problem common to all Primos commands called from the Subsystem. If you want to record the terminal output of the Fortran compiler, you must use the Primos command output facility. This facility is accessed through the Subsystem 'como' command; for details, see the [ul Software Tools Subsystem Reference Manual] or use the 'help' command. .SH "Linking" The last step in preparing the program for execution is linking. The linking step fixes the memory locations of the Subsystem common areas; assigns the binary module for each subprogram to an absolute memory location; and links in the required Subsystem support routines, Fortran run-time routines, and Primos system calls. The memory image file produced by this step may then be executed. It should be noted here that programs linked under the Subsystem can run .ul only under the Subsystem; they may not run without it. .pp The 'ld' command is used to invoke the Primos loader to to do the linking. Its syntax is as follows: .be ld [-u] . . . [-l ] . . . [-t -m] [-o ] .ee This is not the entire syntax accepted by 'ld,' but a complete discussion requires detailed knowledge of the Primos loaders. For more information, see the Subsystem reference manual. .pp The "-u" option causes the loader to print a list of undefined subprograms. Any number of binary files to be included may be listed. The only restriction is that the main program .ul must be the first binary subprogram encountered -- it must be the first program unit in a binary file, and that binary file must be the first to appear on the command line. Any number of libraries (residing in "=lib=") may then be specified with the "-l" option. The "-t -m" options cause a load map to be produced on a file with the name as the output file (or first , if an output file is not specified) with ".m" appended. If the file name ends with ".b", the ".b" is replaced by the ".m". The "-o" option specifies the name of the output file. If the "-o" option is omitted, the output file will have the same name as the first , with ".o" appended. If the name of the first ends in ".b", the ".b" will be replaced by the ".o". .pp Even though linking is a mysterious process, it need not be traumatic. Most of the time, you will be linking a single binary file with no additional libraries. For instance, if you had a binary file named "prog.b," you could produce an object program by just typing the command .be ld prog.b .ee The Primos loader would be invoked, and after a great deal of garbage was printed on the terminal, the executable program "prog.o" would be produced. .pp The only thing that you must do is look for the message "LOAD COMPLETE" lurking somewhere near the end of this garbage. If you find this message, it means that all of the external references in your program (subroutine and function calls) have been satisfied, and linking is complete. If you don't find this message, there are unsatisfied references in your program. You may then call 'ld' with the "-u" option and the loader will print the names of the unsatisfied references on the terminal. You will probably then find that these references are caused by misspelled subprogram names, missing subprograms, or undimensioned arrays (remember, the Fortran compiler treats undimensioned arrays as functions calls, so you may not always get an error message from the compiler). .ne 20 .pp Again, for completeness, here are some examples of 'ld' at work: .be .ne 16 # link the binary files "p1.b", "p2.b", and "p3.b" # to produce "p1.o" as output ld p1.b p2.b p3.b # link the binary file "nprog.b", [cc]mc | # include the library "vshlib", [cc]mc # and produce the output file "nprog" [cc]mc | ld nprog.b -l vshlib -o nprog [cc]mc # link the binary files "np1" and "np2", # produce a load map, # and output "my_new_prog" ld np1 np2 -t -m -o my_new_prog .ee .pp The Primos loader also pays no attention to i/o redirection. If you want to catch its terminal output, you must use the Primos 'como' commands. For details, see the reference manual or use the 'help' command. .SH "Executing" Executing a Subsystem program is the easiest step of all. All you have to do to execute it is to type its name. For instance, if your object program was named "prog.o", all you need type is .be prog.o .ee to make it go. Because the shell also looks in your current directory for executable programs, "prog.o" is now a full-fledged Subsystem command. You may give it arguments on its command line, redirect its standard inputs and outputs, include it in pipelines, or use it as a function. Of course to be able to do all of these things properly, it must observe the Subsystem conventions and use the Subsystem I/O routines. .SH "Shortcuts" There are several shortcuts that speed things up and save typing when developing programs. .PH "Shell Programs" Shell programs can be a great help when performing repetitive tasks. Quite often one of these tasks is preprocessing, compiling, and linking a program during its development. A simple shell program can save a great deal of typing in this situation. For instance, let's say we are writing a Ratfor program that is in the file "np.r". We are in the process of adding new features to "np" and will probably compile and test it several times. We can make a very simple shell program that will keep us from having to type 'rp,' 'fc,' and 'ld' commands every time we want to make a test run. All we have to do make a file containing these three commands with 'cat': .be .ne 6 .fi ] .bf cat >cnp .nf .bf rp np.r .bf fc np.f .bf ld -u np.b -o np .bf ] .ee Now the file "cnp" contains the following text: .be .ne 3 rp np.r fc np.f ld -u np.b -o np .ee All we need do now to preprocess, compile, and link our program is just type the name of the shell program as a command: .be cnp .ee and the shell will execute all of the commands contained in it. .PH "The 'Rfl' Command" Of course, it is so common to preprocess, compile, and link a program, there is an already-built shell program that works nicely in most cases. 'Rfl' contains the necessary commands to preprocess, compile and link a Ratfor program contained in a file whose name ends with ".r". All you have to do is type .be rfl np.r .ee and 'rfl' will execute the necessary commands to produce an executable file named "np". (note that the executable file is named "np" and not "np.o"!) 'Rfl' can also do some other handy things that you can find out about in the Subsystem reference manual. .PH "Storing Source Programs Separately" When you write fairly large programs or test modules independently, it is often convenient to store the programs in separate files. If this is the case, creating an executable program is just a little bit more complicated. The easiest solution is to just name all of the programs on the 'rp' command line, like this: .be rp p1.r p2.r p3.r .ee 'Rp' will preprocess all of the files together and produce output on the file "p1.f". The [bf define] statements in "p1.r" will still be in effect when "p2.r" is preprocessed, etc. so "p1.r", "p2.r", and "p3.r" might just as well be together in one file. .PH "Compiling Programs Separately" A little bit harder, but sometimes much faster, is to preprocess and compile the modules separately and then combine them during linking. There are two things that you have to watch. The first problem with separate compilation is that [bf define] statements in one file cannot affect subprograms in the other files. For a large program that would benefit from separate compilation, this nastiness can be avoided by placing all of the [bf defines] together in one file and placing an [bf include] for that file at the beginning of each of the files containing the program. The [bf defines] will then be applied uniformly to all parts of the program. .pp The second thing is that since Ratfor chooses unique Fortran names in the order it is presented with "long" Ratfor names, it cannot guarantee that a long name in one file will be transformed into exactly the same Fortran name as the same long name in a second file (although the probability is quite high). To avoid problems, either subprogram names that are cross-referenced in the separate binary files should be given six-character or shorter names, or a [bf linkage] declaration containing the names of all subroutines, function, and common blocks should be inserted at the beginning of each module. It is usually easiest to handle the [bf linkage] declaration just like the [bf define] statements: put it in a separate file, and add an [bf include] statement for it at the beginning of each module. .pp Then, the program units in each file may be preprocessed and compiled separately. The binary files from the separate compilations are linked together by just listing the names of all of the files on the 'ld' command: .be ld p1.b p2.b p3.b .ee The only restriction is that the main program .ul must appear first. The object file from this example would be named "p1.o", but this could have been overridden by including the "-o " option. .pp When compiling parts of a program separately, you should be aware that incorrect use of the [bf linkage] declaration can cause totally irrational behavior of the program with no other indication of error. Since no checking is done on the [bf linkage] declaration, you must be certain that every external name appears in the statement. More importantly, when you add a subroutine, function, or common block, you must remember to change the [bf linkage] declaration. In addition, if you do not add the name to the very end of the declaration, you must immediately recompile all modules! If you compile separately, and are confronted with a situation in which your program is misbehaving for no apparent reason, re-check the [bf linkage] declaration and recompile all the modules. .SH "Debugging" Debugging unruly programs under Primos is at best a grueling task, as currently there is almost no run-time debugging support. Except for a couple of machine-language level debuggers, you'll get very little help from Primos (except for some nasty error messages) while debugging programs. This means that such techniques as top-down design, reading other programmers' code, and reasonably careful desk checking will pay off in the long run. But even with all the care in the world, some bugs will creep through (especially on an unfamiliar system). The next few paragraphs will be devoted to techniques for exterminating these stubborn bugs. .pp For an experienced user, a load map, the Primos DMSTK command, and VPSD (the V-mode symbolic debugger) can very quickly isolate the location, if not the cause, of a bug. With more complicated programs that are dependent on the internal structure of the machine and operating system, such machine level debugging cannot always be avoided. If you find yourself in such as position, you can begin to learn some of these things by examining the following reference manuals: .in +10 .tc \ .ta 11 .sp .ti -10 MAN 1671\System Reference Manual, Prime 100-200-300 .sp .ti -10 MAN 2798\System Reference Manual, Prime 400 .sp .ti -10 FDR 3059\The PMA Programmer's Guide .sp .ti -10 FDR 3057\User Guide for the Fortran Programmer .sp .in -10 .pp Most often, the bug can be found by one or more of the following techniques: .sp .tc \ .ta 6 .in +5 .ti -5 (1)\Inserting 'print' calls to display the intermediate results within the program. .sp .ti -5 (2)\Using the Ratfor subroutine trace. .sp .ti -5 (3)\Using the Fortran statement number and assignment trace. .sp .in -5 It is usually quickest to use the Ratfor subroutine trace (by including the "-t" option on the 'rp' command line). Although this trace lists only subroutine nesting, it will narrow down where a program is blowing up to a single subprogram. If the program is very modular and contains mostly small subprograms, quite often, the error can be spotted. .pp If the Ratfor trace fails to pinpoint the problem, the Fortran statement and assignment trace will give a great deal more information (possibly hundreds of pages). The Fortran trace can be produced by specifying the "-t" option on the 'fc' command. The Fortran code produced by 'rp' must be examined to locate the statement numbers, but given the large number of statement labels generated by 'rp,' study of this trace can isolate the problem practically to within one statement. .pp The above debugging methods are quick and easy to use when the program contains a catastrophic error that causes an error termination or an infinite loop. While this is sometimes the case, more often a subtle error is the problem. In finding these errors, there is no substitute for carefully inserted debugging code (such as calls to 'print') at critical points in the program. .pp The rest of this section is devoted to a brief description of many of the terminal errors that may do away with programs (and the Subsystem). Most terminal errors cause the Subsystem command interpreter to be terminated along with the user's delinquent program. You can tell that you've been booted into Primos by the appearance of the "OK," or "ER!" prompt. All error messages that cause an exit to Primos are briefly explained in appendix A-4 of the Prime Fortran Programmer's Guide (FDR3057). Some very common programming errors can cause cryptic error messages with explanations that are close to unintelligible. Hopefully, most of these messages are described below. .pp Many Primos error messages are dead giveaways of program errors. Messages that begin with four asterisks are from the Fortran runtime packages -- they usually indicate such things as division by zero or extraction of the square root of a negative number. For example, .be **** SQRT -- ARGUMENT < 0 OK, .ee results from extracting the square root of a number less than zero. .pp Other, more mysterious, error messages can also be caused by simple program errors. .be Error: condition "POINTER_FAULT$" raised at .ee can be caused by referencing a subprogram which has not been included in the object file. An obvious indication of a missing subprogram is the failure to get the .be LOAD COMPLETE .ee message from 'ld'. (Note that the Fortran compiler treats references to undimensioned arrays as function calls!) A more insidious cause of the "POINTER FAULT" message is a reference to an unspecified argument in a subprogram; i.e. the calling routine specifies three arguments and the called routine expects four. The error occurs when the unspecified argument is .ul referenced in the subprogram, not during the subprogram call. .be .in -5 Error: condition "ACCESS_VIOLATION$" raised at Error: condition "RESTRICTED_INST$" raised at Error: condition "ILLEGAL_SEGNO$" raised at Error: condition "ARITH$" raised at Program halt at .in +5 .ee all can result from a subscript exceeding its bounds. Because the program may have destroyed parts of its code, the memory addresses sometimes given may well be meaningless. Even so, you may locate the routine in which the program blew up by using the Primos DMSTK command and a load map. For instance, given the following scenario (ellipsis indicate irrelevant information), .be .in -5 Error: condition "POINTER_FAULT$" raised at 3.4000.001000. Abort (a), Continue (c) or Call Primos (p)? [bf p] OK, [bf dmstk] ... Stack Segment is 6002. 6) 001464: Condition Frame for "POINTER_FAULT$"; ... Raised at 3.4000.017202; LB= 0.4000.017402, ... 7) 001374: Fault Frame; fault type= 000064 .fi Returns to 3.4000.017202; .ul LB= 0.4000.017402, ... .nf Fault code= 100000, Fault addr= 3.4000.017204 Registers at time of fault: ... .in +5 .ee .fi The numbers following "LB=" on the underlined portion of the stack dump show the address of the data area of the procedure executing when the fault occurred. The segment number portion of this address (the four-digit part) tells who the routine belongs to: .be .ta 14 .ul Segment\Use 0000 - 0033\Operating System 2030\Software Tools Shell 2031\Software Tools Screen Editor 2035\Software Tools Library 2050\Fortran Library 4000 - 4037\User Program 4040\Software Tools Common 4041\Software Tools Stack 6001\Fortran Library 6002\Primos Ring 3 Stack .ee If the executing routine is not part of your program, you can trace back the stack (see below) until you find which of your subprograms made the call. If the segment number begins with "4", you need only look down the right-most two columns of the load map (see the 'ld' command) for the two numbers (4000 17402 in this case). If you get an exact match, just look across to the name on the left -- this is the subprogram that was executing. Otherwise, if none of the numbers match then either the program has clobbered itself and jumped into nowhere, you left off an argument to a library subprogram, or one of the library routines has caused an exception trap with no fault vector. .pp Subsequent entries in the stack dump (following the information in the last scenario) can be used to find what procedure calls were in process when the error occurred. The entries are of the following form: .be .in -5 Stack Segment is 4041. 8) 002222: Owner= (LB= 0.4000.017402). Called from 3.4000.017700; returns to 3.2035.017702. 9) 002156: Owner= (LB= 0.4000.013026). Called from 3.4000.013442; returns to 3.2030.013450. ... .in +5 .ee Each entry on the Subsystem stack (segment 4041) represents a procedure call in process. You can use the numbers following the "LB=" and the load map to trace back through the "stack" of procedure calls, just as with the "fault frame" mentioned above. .pp If you find yourself at a complete and total loss at finding why your program is blowing up, here is a list of some of the errors that have caused us great anguish: .sp .in +5 .tc \ .ta 6 .ti -5 -\Subscript out of range. This error can cause any number of strange results. .sp .ti -5 -\Undefined subprogram. This error can be detected by the lack of a "LOAD COMPLETE" message from the 'ld' command. .sp .ti -5 -\Too few arguments passed. This error almost always causes a "POINTER_FAULT$" when the missing argument is referenced. .sp .ti -5 -\Code and initialized local data requires more that one segment (64K words). The load map shows how much space is allocated. No linkage or procedure frame should appear in any segment other than 4000. .sp .ti -5 -\Delimiter character is missing in a packed string. This includes periods in packed strings passed to 'print' and 'input'. This error causes the program to run wild, writing all over the place. .sp .ti -5 -\Type declaration is missing for a function. This error can causes failure of routines such as 'open' which return an integer result. The Primos Fortran compiler does not flag undeclared functions. This error may also cause an erratic real-to-integer conversion error or cause the program to take an exception trap. .sp .ti -5 -\A subprogram is changing the value of a constant. If you pass a single constant as a function or subroutine argument, and the subprogram changes the corresponding parameter, the values of all occurrences of that constant in the calling program will be changed. With this error, it is quite possible for the constant 12 to have the value -37 at some time during execution. .sp .in -5 .SH "Performance Monitoring" In most cases, it is very difficult to determine how much processing time is required by different parts of a program. Since it is nearly impossible to determine which parts of a program are "inefficient", especially before the program is written, it often more effective to write a program in the most simple and straightforward manner, and then use performance monitoring tools to find where the program is spending its time. It has many times been our experience to find even though parts of a program are coded inefficiently, only a very small amount of time is wasted. .pp There are two available methods for obtaining an execution time "profile" of a Ratfor program. The first method provides statistics on the number of calls to and the amount of time spent in each subprogram. The second method provides a count of the number of times each statement in the program is executed. .pp To invoke the subroutine profile, just preprocess (in one run) all the subprograms to be profiled. Add the "-p" option to the 'rp' command line when the programs are preprocessed. Then compile, link and execute the program normally. When the program terminates (it must execute a [bf stop] statement, and not call "error"), type the command .be profile .ee 'Profile' accesses the files "timer_dictionary" (output by 'rp') and "_profile" (output by your program) and prints the subroutine profile to standard output. .pp To invoke the statement count profile, put all the subprograms to be profiled (you must also include the main program) in a single file. Then preprocess the file with 'rp' and the "-c" option. Compile, link, and execute the program. When the program terminates normally, type the command .be st_profile myprog.r .ee (Of course, assuming your source file name is "myprog.r".) A listing of the program with execution count for each line will be printed. .pp When running a profile, there are several things to keep in mind. First, the program with the profiling code can be more than twice as large as the original program. Second, the program can run an order of magnitude more slowly. Third, there can be a considerable delay between the execution of the [bf stop] statement and the actual end of the program. Finally, you should remember that the main program and all subprograms to be profiled must be preprocessed at the same time. .SH "Conditional Compilation" Conditional compilation is a handy trick for inserting debugging code or setting compile-time options for programs. Conditional compilation can be approximated in Ratfor by defining an identifier, such as "DEBUG" to a sharp sign or null (for off and on respectively). Lines in the Ratfor program beginning with the identifier "DEBUG" (i.e. debugging code) are not compiled if "DEBUG" is defined to be "#", but are compiled normally if "DEBUG" is defined as a null string. .pp For instance, the following example shows how conditional compilation can be used to "turn off" print statements at compile time: .be define (DEBUG, #) fd = open (fn, READ) DEBUG call print (ERROUT, "fd returned:*i*n"s, fd) ... len = getlin (str, fd) DEBUG call print (ERROUT, "str read: *s"s, str) .ee In this example, all lines beginning with "DEBUG" are ignored, unless the [bf define] statement is replaced with .be define (DEBUG, ) .ee Then, all lines beginning with "DEBUG" will be compiled normally. .SH "Portability" If your intent is to produce portable Fortran code, the Ratfor preprocessor, 'rp' can be invoked with the following four options: .be .fi .HI 5 [bf -h] Produce Hollerith-format string constants rather than quoted string constants. This option useful in producing character strings in the proper format needed by your Fortran compiler. .HI 5 [bf -v] Output "standard" Fortran. This option causes 'rp' to generate only standard Fortran constructs (as far as we know). This option does not detect non-standard Fortran usage in Ratfor source code; it only prevents 'rp' from generating non-standard constructs in implementing its data and control structures. .HI 5 [bf -x] Translate character codes. 'Rp' uses the character correspondences in a translation file to convert characters into integers when it builds Fortran "data" statements containing EOS-terminated or PL/I strings. If the option is not specified, 'rp' converts the characters using the native Prime character set. .HI 5 [bf -y] Do not output "call swt". This option keeps 'rp' from generating a "call swt" in place of all "stop" statements, which are required for Fortran programs to run under the Subsystem. .ee The following option for 'fc' may also help: .be .fi .HI 5 [bf -i] Consider all integers to be "long" (32-bit) rather than short. .ee .MH "Source Program Format Conventions" After considering many program formatting styles, we have concluded that the convention used by Kernighan and Plauger in [ul Software Tools] is the most expedient in terms of clarity and ease of modification. As a consequence, we have tried to be consistent in the use of this convention throughout the Subsystem to provide uniformly readable and modifiable code. We present the convention here in the hope that you can use it to the same advantage. .SH "Statement Placement" The placement of statements in program units is perhaps the most important part of the formatting convention. Through uniform placement of statements, many documents can be produced directly directly from the source code. For instance, the skeleton for Section 2 of the Subsystem Reference Manual was produced originally from the subprogram headers of the Subsystem library subprograms. Then the detail was filled in using the text editor. .pp The order of a program unit (including a main program) should be as follows: .ta 6 .tc \ .in +5 .rm -5 .lt +5 .sp .ti -5 1.\A comment line of the following format: .sp .nf # --- .fi .sp .ti -5 2.\The [bf subroutine] or [bf function] statement (or nothing if it is a main program). .sp .ti -5 3.\The declarations of all arguments passed to the subprogram, if any. .sp .ti -5 4.\A blank line .sp .ti -5 5.\Declarations for all local variables in the program unit. .sp .ti -5 6.\A blank line. .sp .ti -5 7.\Executable program statements. .sp .ti -5 8.\The [bf end] statement. .sp .ti -5 9.\Three blank lines. .sp .in -5 .rm +5 Of course, extra blank lines should be used freely to separate different logical groups of declarations and different logical blocks of executable statements. .pp As an example, here is the source code for the subroutine "cant" taken directly from the Subsystem library: .be [cc]mc | # cant --- print cant open file message [cc]mc subroutine cant (str) character str (ARB) call putlin (str, ERROUT) [cc]mc | call error (": can't open.") [cc]mc return end .ee .SH "Indentation" The indentation convention is very simple. It is based on the idea that a statement should be indented three spaces to the right of the innermost statement controlling it. Braces are placed as unobtrusively as possible, without affecting the ease of adding or deleting statements. .pp Statements, with the exception of the program heading comment, are placed three spaces to the right of the left margin. All statements are placed in this position, unless they are subordinate to a control statement. In this case, they are placed three spaces to the right of the beginning of the controlling statement. .pp Braces do not affect the placement of statements. An opening brace is placed on the line with the controlling statement. A closing brace is placed on a separate line three spaces to the right of the beginning of the controlling statement. .pp Multiple statements per line are forbidden, except when a chain of .sb [bf if - else if . . . else] .xb statements is used to implement a case structure. In this event, the [bf else if] is considered a single statement, appearing on the same line, and subsequent lines are indented only three spaces to the right. .pp If all of this seems terribly confusing, here are some examples that show the indentation convention in action (the bars are just to show you the matching of braces): .be .ne 24 for (i = 1; str (i) ~= EOS; i += 1) { | if (str (i) == 'a'c) { | | j = ctoi (str (2), i) | | select (j) | | | when (1) | | | | call alt1 | | | when (2) | | | | call alt2 | | | when (3) { | | | | call alt1 | | | | call alt2 | | | | } | | else | | call error ("number must be >= 1 and <= 3"s) | ---} | else if (str (i) == 's'c) | repeat { | | j = ctoi (str (2), i) | | status = getnext (j) | ---} until (status == EOF) | else { | | call clean_up | | stop | ---} ---} .ee .SH "Subsystem Definitions" The use of the [bf define] statement plays a large part in producing readable, maintainable programs. Hiding implementation details with [bf define] statements not only produces more readable code, but allows changes in the implementation details to be made without necessitating changes in applications programs. The development of a large part of the Subsystem would have been greatly hindered if it had not been possible to redefine the constant "STDIN" from "1" to "-11", with no more than recompilation. .pp The Subsystem definitions file, "=incl=/swt_def.r.i" exists primarily to hide the dirty details of the Subsystem support routines from Ratfor programmers. We sincerely believe that the character string "EOF" is inherently more meaningful than the string "-1". (Would you believe that after three years of using the Subsystem, the author of this section had to look up the value assigned to "EOF" in order to write the preceding sentence?) .pp Of course, the use of the Subsystem definitions also allow the developers to change these values when necessary. Of course, these changes force recompilation of all existing programs, but we feel that this is a small price to pay for the availability of more advanced features. All users of the Subsystem support routines are therefore warned that the values of the Subsystem definitions may change between versions of the Subsystem. (At Georgia Tech, this may be daily.) Programs that depend on the specific values of the symbolic constants may well cease to function when a new version of the Subsystem is installed. .pp Appendix D contains specific information about (but not specific specific values for) the standard Subsystem definition file. As a general rule, all symbolic constants mentioned in Section 2 of the Subsystem Reference Manual can be found in "=incl=/swt_def.r.i". .MH "Using the Subsystem Support Routines" Many of the capabilities available to a Subsystem programmer are provided through the Subsystem support routines. The Subsystem support routines consist of well over one hundred Ratfor and PMA subprograms that either perform common tasks, insulate the user from Primos and Fortran, or conceal the internal mechanisms of the Subsystem. By default, the library containing all of these routines ("=lib=/vswtlb") is included in the linking of all Subsystem programs. Therefore, no special actions need be taken to call these routines. .pp If you notice that there are some "holes" in the functionality of the Subsystem library, you are probably quite correct. The Subsystem library has grown to its present size through the effort of many of its users. The instance often arises that a routine is required to fill a specific function. In keeping with the .ul Software Tools methodology, instead of writing a very specific routine, we ask that the author write a slightly more general routine that can be used in a variety of instances. The routine can then be documented and placed in the Subsystem library for the benefit of all users. Many of the support routines, including the dynamic storage management routines, have come from just such instances. The "holes" in the Subsystem library are just waiting for someone to fill them; if you need a routine that isn't there, please write it for us. .SH "Termination" The subprogram 'swt' terminates the program and causes a return to the Subsystem command interpreter. Any Subsystem files left open by the program are closed. Ratfor automatically inserts a "call swt" any time it encounters a Fortran [bf stop] statement. All Ratfor programs should [bf stop] rather than "call exit". Fortran and PMA programs should invoke 'swt' to terminate. .SH "Character Strings" Most of the support routines use characters that are unpacked, one per word (i.e. integer variable), right-justified with zero fill, rather than the Fortran default, two characters per word, left-justified, with blank fill (for an odd last character). In addition to the simplicity of manipulating unpacked strings, the unpacked format represents characters as small, positive integers. Thus, character values can be used in comparisons and as indexes without conversion. .pp Most of the support routines that manipulate character strings expect them to be stored in an integer array, one character per word, right-justified and zero-filled, and terminated with a word containing the symbolic constant 'EOS'. Strings of this format are usually called EOS-terminated strings. .pp Support for the use of unpacked characters is provided in several ways: (1) the Subsystem I/O routines perform conversion to and from unpacked format, (2) single-character constants 'a'c, 'b'c, ','c, etc. are provided for use in place of single-character Hollerith literals, and (3) the Ratfor [bf string] statement is provided to initialize EOS-terminated strings. .pp In a few cases, it is more convenient to use a Hollerith literal instead of an EOS-terminated string. Since it is impossible to tell the length of a Hollerith literal at run time, Hollerith literals used with the Subsystem are required to contain a delimiter character (usually a period) as the last character. Hollerith literals or integer arrays that contain Hollerith-format characters and end with a delimiter character are referred to as packed strings. .pp Following are brief descriptions for the most generally useful character manipulation routines. For specific information, see the .ul Software Tools Subsystem Reference Manual. .PH Equal 'Equal' is an integer function that takes two EOS-terminated strings as arguments. If the two strings are identical, 'equal' returns YES; otherwise it returns NO. For example, .be string dash_x "-x" integer equal ... if (equal (argument, dash_x) == YES) call cross_ref .ee .PH Index 'Index' is used to find the position of a character in an EOS-terminated string. If the character is in the string, its position is returned, otherwise zero is returned. 'Index' is very similar to the built-in function of the same name in PL/I. Example: .be 14 string options "acx" integer ndx integer index ... ndx = index (options, opt_character) select (ndx) when (1) call list_all when (2) call list_common when (3) call cross_reference else call remark ("illegal option"s) .ee This example selects one of a number of subroutines to be executed depending on a single-character option specifier. Of course, this particular example could be done with just [bf select] alone. 'Index' is also useful in character transliteration and conversion from character to binary integer. .PH Length 'Length' is an integer function that returns the length of an EOS-terminated string. The length of a string is zero if and only if its first character is an EOS; it is the number of characters before the EOS in all other cases. 'Length' is often useful in deciding where to start appending additional text, as in the following example: .be integer len integer length ... len = length (str) call scopy (new_str, 1, str, len + 1) .ee .PH "Mapdn and Mapup" These functions accept a single character as an argument and if the character is alphabetic, force it to lower or upper case, respectively. 'Mapdn' and 'mapup' quite often find use in mapping option letters to a single case before comparison. Since non-alphabetic characters are not modified, these routines may be used safely even if non-alphabetic characters appear. In addition, these routines provide a very good place to isolate character set dependencies. For example, .be character c character mapdn ... if (mapdn (c) == 'a'c) { # handle 'a' option ... else if (mapdn (c) == '1'c) { # handle '1' option .ee .PH "Mapstr" 'Mapstr' provides case mapping for alphabetic characters in EOS-terminated strings. As arguments 'mapstr' takes a string and the symbolic constant 'LOWER' or 'UPPER'. Alphabetic characters in the string are then forced to lower or upper case, depending on the constant specified. .PH "Scopy" The subroutine 'scopy' is used for copying EOS-terminated strings. It requires four arguments: the source string, the position from which to start copying, the destination string, and the position at which filling begins in the destination string. Since Ratfor provides no string assignment, 'scopy' is normally used to provide the capability. The simple movement of a string from one place to another is coded as .be character str1 (MAXLINE), str2 (MAXLINE) ... call scopy (str1, 1, str2, 1) .ee 'Scopy' is also capable of appending one string to another, as in the following example: .be character str1 (MAXLINE), str2 (MAXLINE) ... call scopy (str1, 1, str2, length (str2) + 1) .ee Note that 'scopy' makes no attempt to avoid writing past the end of 'str2'! .PH "Type" 'Type' is another of the routines that is intended to isolate character dependencies. Type is a function that takes a single character as an argument. If that character is a letter, 'type' returns the constant 'LETTER'; if the character is a digit, 'type' returns the constant 'DIGIT'; otherwise, 'type' returns the character. 'Type' often finds use in a lexical analyzer: .be 12 character c character type if (type (c) == LETTER) { # collect identifier ... else if (type (c) == DIGIT) { # collect integer ... else { # handle special character .ee .SH "File Access" File access is one of the more important aspects of the Subsystem. It is through the Subsystem i/o routines that device independence and i/o redirection are accomplished; moreover, the Subsystem routines provide a much less complicated interface than comparable Primos routines. .pp The basic method of access to a Subsystem file is through the contents of an integer variable called a [bf file descriptor.] File descriptors can be set by one of several routines or they can be set to one of the six standard descriptors representing the six standard ports provided to all Subsystem programs. .pp Quite often, the standard ports provide all of the file access required by a program. Values for the standard port descriptors can be accessed from [bf defines] contained in "=incl=/swt_def.r.i" ('Rp' automatically includes this file in each run). The following table gives the symbolic names for the three standard input and three standard output ports available: .ne 7 .be .ul Input Ports Output Ports STDIN1 (or STDIN) STDOUT1 (or STDOUT) STDIN2 STDOUT2 STDIN3 (or ERRIN) STDOUT3 (or ERROUT) .ee These constants may be used wherever a file descriptor is required by a Subsystem i/o routine. .pp Other files may be accessed or created through the routines 'open', 'create', and 'mktemp' that are described later. At the moment, it is sufficient to say that these routines are functions that return a file descriptor that may be used in other Subsystem i/o calls. .pp Once a file descriptor has been obtained, the file it references may be read with the routines 'getlin', 'getch', or 'input'; written with the routines 'putlin', 'putch', or 'print'; positioned with the routines 'wind' or 'rewind'; or closed with the routines 'close' or 'rmtemp'. .PH "Open and Close" 'Open' takes an EOS-terminated path name and a mode (one of the constants READ, WRITE, or READWRITE) as arguments and returns the value of a file descriptor or the symbolic constant ERR as a function value. 'Open' is normally used to make a file available for processing in the specified mode. If the mode is READ, 'open' will open the file for reading; if the file doesn't exist or cannot be read (i.e. no read permission), 'open' will return ERR. If the mode is WRITE or READWRITE, 'open' will open an existing file or create a new file for writing or reading and writing, if possible; otherwise it will return ERR. If 'open' opens an existing file, it will never destroy the contents, even if mode is WRITE. To be certain that a "new" file is empty, use 'create' instead of 'open'. .pp 'Close' takes a file descriptor as its argument; it closes and releases the file attached to the descriptor. If 'close' is called with a standard port, it takes no action. .pp Opening and closing a file is really very easy. This example opens a file named "=extra=/news/index" and returns the file descriptor in 'fd'. If the file can't be opened, the program will terminate with a call to 'cant'. .ne 14 .be file_des fd integer open string fn "=extra=/news/index" fd = open (fn, READ) # open "=extra=/news/index" if (fd == ERR) call cant (fn) call close (fd) # release the file stop .ee If the file can't be opened, 'cant' will print the message .be =extra=/news/index: can't open .ee and terminate the program. .PH Create 'Create' takes the same arguments as 'open', but also truncates the file (makes it empty) to be sure that there are no remnants of its previous contents. .PH "Mktemp and Rmtemp" Quite often, programs need temporary files for their internal use only. 'Mktemp' and 'rmtemp' allow the creation of unique temporaries in the directory "=temp=". 'Mktemp' requires only a mode (READ, WRITE, or READWRITE) as an argument and returns a file descriptor as its function value. 'Rmtemp' takes a file descriptor as its argument and destroys and closes the temporary file. (One should use caution, for if a descriptor for a permanent file is passed to 'rmtemp', that file will also be destroyed.) .pp Typical use of 'mktemp' and 'rmtemp' usually involves the writing and reading of an intermediate file: .ne 14 .be file_des fd integer mktemp fd = mktemp (READWRITE) # create a temporary file call rewind (fd) # reposition the temporary call rmtemp (fd) # close and destroy the temporary .ee .PH "Wind and Rewind" The subroutines 'wind' and 'rewind' allow the positioning of an open file to its end and beginning, respectively. Both take a file descriptor as an argument. Usually, 'rewind' is used when a program creates a file and then wishes to read it back; 'wind' is often used when a program wants to add to the end of an existing file. .pp A program wishing to extend a file would make a call to 'wind' just after successfully opening the file to be extended: .ne 11 .be file_des fd integer open string fn "myfile" fd = open (fn, READWRITE) if (fd == ERR) call cant (fn) call wind (fd) # file is now positioned at the # end, ready for appending. .ee .PH Trunc 'Trunc' truncates an open file. Truncating a file means releasing all of its disk space, hence making it empty, but retaining its name and attributes. 'Trunc' takes a file descriptor as its argument. .PH Remove 'Remove' removes a file by name, deleting it from the disk directory. It takes an EOS-terminated string as its argument, and returns the constant OK or ERR, depending on whether or not it could remove the file. ('Remove' will also delete a Primos segment directory without complaining.) .PH Cant 'Cant' is a handy routine for handling exceptions when opening files. For its argument, 'cant' takes an EOS-terminated string containing a file name. It prints the message .be : can't open .ee and then terminates the program. .PH Getlin All Subsystem character input is done through 'getlin'. 'Getlin' takes a character array (at least MAXLINE long) and a file descriptor and returns a line of input in the array as an EOS-terminated string. Although the last character in the string is normally a NEWLINE character, if the line is longer than MAXLINE, no NEWLINE will be present and the rest of the line will be obtained on the next call to 'getlin'. For its function value, 'getlin' returns the length of the line delivered, (including the NEWLINE, if any) or the constant EOF if end-of-file was encountered. .pp Most line-oriented i/o is done with 'getlin'. For instance, using 'getlin' with its analog 'putlin', a program to select only those lines beginning with the letter "a" can be written very quickly: .ne 8 .be character buf (MAXLINE) integer getlin while (getlin (buf, STDIN) ~= EOF) if (buf (1) == 'a'c) call putlin (buf, STDOUT) .ee 'Getlin' is guaranteed to never return a line longer than the symbolic constant MAXLINE (including the terminating EOS). .pp If needed, there are a number of routines that you can call to convert the character string returned by 'getlin' into other formats, such as integer and real. Most of these routines are described later in the section on "Type Conversion". .PH Getch 'Getch' returns one character at a time from a file; it requires a character variable and a file descriptor as arguments; it returns the character obtained, or the constant EOF, in the supplied argument and as the function value. Calls to 'getch' and 'getlin' may be interleaved; 'getlin' will pick up the rest of a line not read by 'getch'. .pp 'Getch' is very useful in lexical analyzers or just when counting characters. For instance, the following routine counts both characters and lines at the same time: .ne 13 .be character c integer c_count, l_count integer getch c_count = 0 l_count = 0 while (getch (c, STDIN) ~= EOF) { c_count = c_count + 1 if (c == NEWLINE) l_count = l_count + 1 } .ee This example assumes that since each line ends with a NEWLINE character, lines can be counted by counting the NEWLINEs. .PH Input 'Input' is a rather general routine created to provide easy access to both interactive and file input. For interactive input, 'input' will prompt at the terminal, accept input, and call the proper conversion routines to produce the desired data formats. In case of unexpected input (like letters in an integer), it will ask for a line to be retyped. For file input, 'input' recognizes that its input is not coming from a terminal (even if from a standard port) by turning off all prompting. It will then accept fixed or variable-length fields from the file under control of the format string. .pp 'Input' requires a variable number of arguments: a file descriptor, a format string, and as many destination fields as required by the format string. It returns the constant EOF as its function value if it encountered end-of-file; otherwise it returns OK. .pp The file descriptor passed to 'input' describes the file to be read. All prompting output (if any) always appears on the terminal. The format string passed to 'input' indicates what prompting information is to be output and what data format to expect as input. Prompts to be output are specified as literal characters; i.e. to output "Input X:", the characters "Input X:" would appear in the format string. Prompting characters may only appear at the beginning of the string and immediately after "skip-newline" ("*n") format codes. Data items to be input are described by an asterisk followed by optionally one or two numbers and a letter. For instance the code to input a decimal integer would be "*i" and the code to input a double precision floating point number would be "*d". .pp When a call to 'input' is executed, the format string is interpreted from left to right. When leading literal characters are encountered, they are output as a prompt. When the first format code is encountered, a line is read from the file, the corresponding item is obtained from the input line, and the item is placed in the next item in the argument list. More items are removed from the input line until the end of the format string is reached or a newline appears in the input. If the end of the format string is encountered, the rest of the input line is discarded, and 'input' returns OK. Otherwise, if a newline is encountered in the input, fields designated by the format are filled with empty strings, blanks, or zeroes, until the format string is exhausted, or a code ("*n") to skip the NEWLINE and read a new line is encountered. .pp The format string must contain exactly as many input indicators as there are receiving data items in the call. In any case, the maximum number of input items per call is 10. .pp Before we go any further, here is an example of an 'input' call to obtain three integers: .ne 3 .be call input (STDIN, "Type i: *i*nType j: *i*nType k: *i"s, i, j, k) .ee If this statement were executed the following might appear at the terminal (user input is boldfaced): .be .fi Type i: .bf 22 .br Type j: .bf 476 .br Type k: .bf 1 .nf .ee We could also type all three integers on the same line, and 'input' would omit the prompting for the second and third numbers: .be .fi Type i: .bf 22 476 1 .fi .ee .pp There are a number of input indicators available for use in the format string. Since there are a large number of them with many available options, only a few are mentioned in the following table. For further information, see the Subsystem reference manual. .sp .in +24 .ta 6 25 .nf .tc \ .ti -24 .ul Item\Data Type\ Input Representation .fi .sp .ti -24 *n\skip newline\If there is a NEWLINE at the current position, skip over it and read another line. Otherwise do nothing. ('Input' will never read more than one line per call, unless this format code is present. .sp .ti -24 *i\16 bit integer\Input an integer with optional plus or minus sign, followed by a string of digits, delimited by a blank or newline. Leading blanks are ignored. The input radix can be changed by preceding the number with "r" (e.g. octal should be expressed by "8r"). .sp .ti -24 *l\32 bit integer\Same as "*i". .sp .ti -24 *r\32 bit real\Input a real number with optional plus or minus sign, followed by a possible empty string of digits, optionally followed by a decimal point and a possibly empty string of digits. Scaling by a power of 10 may be indicated by an "e" followed by an optional plus or minus sign, followed by a string of digits. The number is delimited by a blank; leading blanks are ignored. .sp .ti -24 *d\64 bit real\Same as "*r". .sp .ti -24 *s\string\Input a string of characters delimited by a blank or newline. No more than MAXLINE characters will be delivered, regardless of input size. [cc]mc | Use "*1s" to read in a single character. (Admittedly, this is an inconsistency; there really should be a "*c" format.) [cc]mc .in -24 .pp Fixed size input fields can be requested by placing the desired field size immediately following the asterisk in the format code. For instance, to read three integers requiring five spaces each, you can use the following format string: .be "*5i*5i*5i" .ee You can also change the delimiting character of a field from its default value of a blank. Just place two commas followed by the new delimiter immediately after the asterisk. For instance, two strings delimited by slashes can be input with the following format string: .be *,,/s*,,/s .ee Regardless of the delimiter setting, a newline is always treated as a delimiter. One caution: if the delimiter is not a blank, leading blanks in strings are not ignored. .PH Readf You can use 'readf' to read binary (memory-image) files that were created with 'writef'. 'Readf' is the fastest way to read files, since no data conversion is performed. However, use of 'readf' and 'writef' tend to make a program dependent on machine word size, and hence, non-portable. .pp 'Readf' takes three arguments: a receiving data array, the maximum number of words to be read, and a Subsystem file descriptor. When called, 'readf' attempts to read the number of words requested; if there are not that many in the file, it returns all that are left. If there are no words left in the file at all, 'readf' returns EOF as its function value; otherwise, it returns the number of words actually read as its function value. .PH Putlin 'Putlin' is the primary output routine of the Subsystem. It takes an EOS-terminated string and a file descriptor as arguments, and writes the characters in the string on the file specified by the descriptor. There is no restriction on the length of the input string; 'putlin' will write characters until it sees an EOS. 'Putlin' [bf does not] supply a newline character at the end of the line; if one is to be written, it must appear in the string. For a simple example, see the description of 'getlin'. .PH Putch A single character can be output to a file with 'putch'; it takes a character and a file descriptor as arguments and writes the character on the file specified by the descriptor. Calls to 'putch' and 'putlin' can be interleaved as desired. .PH Print 'Print' is a general output routine that accepts a format string and up to ten output data items. Interpreting the format string, 'print' calls the appropriate type conversion routines to produce character data, and outputs the characters as directed by the format string. 'Print' requires several arguments: a file descriptor; an EOS-terminated format string; and zero to ten output data arguments, depending on how many are required by the format string. .pp The format string contains two kinds of items: literal items which are output when they are encountered, and output items, which cause the next data argument to be converted to character format and output. Literal items are just characters in the string; i.e. to output "X =", the format string would contain "X =". Output items consist of an asterisk, followed by two optional numbers, followed by a letter. For instance an output item for an integer is "*i" and an output item for single precision floating point is "*r". The next example shows the output of three integers: .be call print (STDOUT, "i = *i, j = *i, k = *i*n"s, i, j, k) .ee If this call were executed, the following might be the result: .be i = 342, j = 1, k = -3382 .ee Some of the more useful output items are described in the following table: .sp .in +5 .ta 10 .nf .ti -5 .ul Item\Data Representation .fi .sp .ti -5 *i\short (16 bit) integer .ti -5 *l\long (32 bit) integer .ti -5 *r\single precision (32 bit) real .ti -5 *d\double precision (64 bit) real .ti -5 *p\packed, period-terminated string .ti -5 *s\EOS-terminated string .ti -5 *c\single character .ti -5 *n\newline .in -5 .sp It is possible to exert much more control over the format of output using 'print'; for more information, see the Subsystem reference manual. .PH Writef 'Writef' is the companion routine to 'readf'; it writes words to a binary (memory-image) file. It is the fastest of the output routines, since it performs no data conversion. It is called with three arguments: a data array containing the words to be written, the number of words to write, and a Subsystem file descriptor. Here is an example fast file-to-file copy using 'readf' and 'writef' together. .be 10 integer l, buf (1024) integer readf file_des in_fd, out_fd repeat { l = readf (buf, 1024, in_fd) if (l == EOF) break call writef (buf, l, out_fd) } .ee .PH Fcopy 'Fcopy' is a very simple routine that copies files. You open and position the input and output files and call 'fcopy' with the input and output file descriptors. It then copies lines from the input file to the output file. 'Fcopy' uses a great deal of "secret knowledge" of the workings of the Subsystem input-output routines, and as a consequence, it copies disk-file to disk-file very quickly (even when the descriptors are of standard ports). .PH "Markf and Seekf" 'Markf' and 'seekf' are companion routines that implement random access on disk files. 'Markf' takes a file descriptor as argument and returns a "file_mark" (currently a 32-bit integer). 'Seekf' takes the file mark along with a file descriptor and sets the file pointer so that the file is positioned at the same place as when the "mark" was taken. .pp To be used portably, 'markf' and 'seekf' may only be used between calls to 'readf' and 'writef', or immediately after input or output of a newline character (i.e. at the ends of lines). In addition, a call to 'putlin' or 'putch' on a file effectively (although not actually) destroys information following the current position of the file. For example, if you want to write a line in a file, go off and do other operations on the file, and then be able to re-read the line later, you can use 'markf' and 'seekf': .ne 15 .be 12 file_mark fm file_mark markf file_des fd character line (MAXLINE) fm = markf (fd) call putlin (line, fd) ### perform other operations on 'fd' call seekf (fm, fd) call getlin (line, fd) # get 'line' back .ee .pp Non-portably, you can assume that a "file mark" is a zero-relative word number within the file -- to get word number 12 in the file, just execute .ne 4 .be call seekf (intl (12), fd) call readf (word, 1, fd) .ee (Remember: file marks are 32 bits, not 16! We use 'intl' here to make "12" into a 32 bit integer.) Keep in mind that this "secret knowledge" is useful only with "readf" and "writef", not with any other input or output routine. Blank compression is used in line oriented files, so the position of a line is dependent not only on length of previous lines, but also on their content. This usually makes the position of a line in a file quite unpredictable. .PH Getto 'Getto' exists primarily to interface with the Primos file system calls. 'Getto' takes a path name (in an EOS-terminated string) as its first argument. It follows the path and sets the current directory to that specified for the file in the path name. It then packs the file name into its second argument, a 16 word array (with blank padding), ready for a call to the Primos file system. It fills its 3-word third argument with the password of the last node of the path (if there was one). Its fourth argument, an integer, is set to YES if 'getto' changed the attach point, and NO otherwise. .pp 'Getto' often finds use when functions other than those supported by Subsystem routines need to be performed, such as setting the passwords on a directory: .ne 9 .be integer pfn (16), opw (3), npw (3), pw (3), att integer getto string fn "=vars=/system" if (getto (fn, pfn, pw, att) == ERR) call print (ERROUT, "can't get to *s*n"s, fn) call spas$$ (pfn, 32, opw, npw) # set passwords if (att == YES) call follow (EOS, 0) # attach back to home .ee .SH "Type Conversion" There are a very large number of type conversion routines available to convert most data types into character strings and back. Because keeping up with all the conversion routine names and calling sequences can be quite a chore, two routines 'decode' and 'encode' exist to handle conversion details in a consistent format. These two routines are described at the end of this section. .pp Most of the "character-to-something" routines require at least two arguments. The first argument is usually the character string, and the second is an integer variable indicating the first of the characters to be converted. The result of conversion is then returned as the function value, and the position variable is updated to indicate the first position past the characters used in the conversion. .pp For example, the simplest "character-to-integer" routine, 'ctoi' requires the two arguments mentioned above. Since it skips leading blanks, but stops at the first non-digit character, it can be called several times in succession to grab several blank-separated integers on a line: .be 10 character str (MAXLINE) integer i, k (4), pos integer ctoi ... pos = 1 do i = 1, 4 k (i) = ctoi (str, pos) if (str (pos) ~= EOS) call remark ("illegal character in input"s) .ee This routine will assume unspecified values to be zero, but complain if non-numeric, non-blank characters are specified. .pp Here is a list of all of the currently supported "character-to-something" routines. .in +14 .rm -5 .lt +5 .sp .ta 10 .ti -9 ctoc\Character-to-character; copies character strings and pays attention to the maximum length parameter. .sp .ti -9 ctod\Character-to-double precision real; handles general floating point input. .sp .ti -9 ctoi\Character-to-integer (16 bit); does not handle plus and minus signs; decimal only. .sp .ti -9 ctop\Character-to-packed-string; converts to packed format with no delimiter character. .sp .ti -9 ctor\Character-to-single precision real; handles general floating point input. .sp .ti -9 ctov\Character-to-PL/I-character-varying; converts to PL/I character varying format. .sp .ti -9 gctoi\Generalized-character-to-integer (16 bit); handles plus and minus signs; in addition to program-specified radix, accepts an optional user-specified radix from 2-16. .sp .ti -9 gctol\Generalized-character-to-long-integer (32 bit); handles plus and minus signs; in addition to program-specified radix, accepts an optional user-specified radix from 2-16. .sp .in -14 .rm +5 .pp In addition to the "character-to-something" routines, there are the "something-to-character" routines. Most of these routines require three arguments: the value to be converted, the destination string, and the maximum size allowable. They return the length of the string produced as the function value. An EOS is always placed in the position following the last character in the destination string, but the EOS is not included when the size of the returned string is calculated. .pp Since the functions will accept a sub-array reference for the output string, you may place several objects in the same string. For example, using the "integer-to-character" conversion routine 'itoc', you can place the four integers in the array 'k' into 'str' in character format: .be character str (MAXLINE) integer i, k(4), pos integer itoc ... pos = 1 do i = 1, 4; { pos = pos + itoc (k (i), str (pos), MAXLINE - pos) if (pos >= MAXLINE - 1) # there's no room for any more break str (pos) = BLANK pos = pos + 1 } str (pos) = EOS # cover up the last blank .ee This code will place the four integers in 'str', separated by a single blank. Although all conversion routines leave an EOS in the string, we have to replace it here because we clobber it with the blank. .pp It's worth noting that the maximum size parameter always includes the EOS -- the conversion routine will never touch any more characters than are specified by this parameter. .pp Here is a list of all available "something-to-character" conversion routines: .in +14 .rm -5 .lt +5 .sp .ti -9 ctoc\Character-to-character; copies character strings and pays attention to the maximum length parameter. .sp .ti -9 dtoc\Double-precision-real-to-character; handles general floating point conversions in Basic or Fortran formats. .sp .ti -9 gitoc\Generalized-integer-to-character (16 bit); handles integer conversions; program-specified radix. .sp .ti -9 gltoc\Generalized-long-integer-to-character (32 bit); handles long integer conversion; program specified radix. .sp .ti -9 itoc\Integer-to-character (16 bit); handles integer conversion; decimal only. .sp .ti -9 ltoc\Long-integer-to-character (32 bit); handles long integer conversion; decimal only. .sp .ti -9 ptoc\Packed-string-to-character; accepts arbitrary delimiter character; will unpack fixed length strings if delimiter is set to EOS and maximum is set to (length + 1). .sp .ti -9 rtoc\Single-precision-real-to-character; handles general real conversion in Basic or Fortran formats. .sp .ti -9 vtoc\PL/I-character-varying-to-character; converts PL/I character varying format to character. .in -14 .rm +5 .pp .PH Decode 'Decode' handles conversion from character strings to all other formats. It is written to be used in concert with 'getlin' and other such routines, and as such, has a rather odd calling sequence. It requires a minimum of five arguments: the usual string, and string index; a format string; a format string index and an argument string index. Following are receiving arguments, depending on the data types specified in the format string. In almost all cases, you should just supply variables with a values of 1 for the format index and the argument index. The string index behaves just as it does in all other character-to-something routine -- on successful conversion, it points to the EOS in the string. The specifics of the format string and receiving fields are identical to 'input'. The only differences are that 'decode' returns with OK in the situations in which 'input' would read another line of input, and EOF otherwise, and that all characters in the format string that are not format codes are ignored. .PH Encode 'Encode' is a companion routine to 'decode': it can access all of the something-to-character conversion routines in a consistent way. For arguments it takes a character string, maximum length of the string, a format string, and a varying number of source arguments, depending on the format string. 'Encode' behaves exactly like 'print', except that it puts the converted characters into the string, rather than putting them onto a file. .SH "Argument Access" Programs often find it necessary to access arguments specified on the command line. These arguments can be obtained as EOS-terminated strings, ready for processing or passing to a routine such as 'open'. .PH Getarg 'Getarg' is the only routine that retrieves arguments from the shell's argument buffer. It is called with three arguments: an integer describing the position of the argument desired, a character array to receive the argument, and an integer describing the maximum size of the receiving array. 'Getarg' tries to retrieve the argument in the specified position; if it can, it returns the length of the string placed in the array; if it can't, it returns the constant EOF. 'Getarg' will never write farther in the character array than the size specified in the third argument. .pp Arguments are numbered 0 through the maximum specified on the command line. Argument 0 is the name of the command, argument 1 is the first argument specified, and so on. The number of arguments present on the command line can be determined by the point at which 'getarg' returns EOF. .pp As a short example, here is a program fragment that attempts to delete all files specified as arguments on its command line: .ne 12 .be character file (MAXLINE) integer i integer remove, getarg i = 1 while (getarg (i, file, MAXLINE ~= EOF)) { if (remove (file) == ERR) call print (ERROUT, "*s: cannot remove*n"s, file) i = i + 1 } .ee .PH Parscl In many programs, argument syntax is quite complex. 'Parscl' exists for the benefit of both programmers and users: it makes coding argument parsing simple and it helps keep argument conventions uniform. Of course, to do this, it must automatically enforce certain argument conventions. 'Parscl' and its accompanying macros expect to recognize arguments of a single letter without regard to case. Rather than a lengthy explanation, let's look at an example: For its arguments, a program requires a page length (which should default to 66 if not present), a title (which may also not be present), a flag to tell whether to format for for a printer or a terminal, and a list of file names to process. In this case, a reasonable option syntax is .be prog [-l ] [-t []] [-p] {<file name>} .ee We have used single letter flags to avoid the need for always specifying arguments. Now, in terms of 'parscl', what we have is an "required integer", an "optional string", and a "flag". This means that "-l" cannot be specified without a <page length>, but "-t" can be specified without a <title> (in this case, of course, we would use an empty title). Be sure to note that a "required" argument means that if the letter is specified, it must be followed by a value. It does [bf not] mean that the letter argument must always be present. In other circumstances, we can also have "optional integer" and "required string" arguments. .pp To use 'parscl' in our program, we must first include the argument macros and declare the argument data area: .be 2 include ARGUMENT_DEFS ARG_DECL .ee Then, near the beginning of the main program, we use a macro call to call 'parscl' that contains the syntax of the command line and a "usage" message to be displayed if the command line is incorrect. For our example, we can use .be 2 PARSE_COMMAND_LINE ("l<req int> t<opt str> p<flag>"s, "prog [-l <page len>] [-t [<title]] [-p] {<file}>"s) .ee For "optional integer" and "required string" arguments, the argument types are "<opt int>" and "<req str>", respectively. .pp If the command line is parsed successfully, 'parscl' returns and the program continues; otherwise, 'parscl' prints the "usage" message with a call to 'error'. Once 'parscl' has returned, we can set the default values, test for the presence or absence of arguments, and obtain values of arguments. First we usually set default values: .be 5 ARG_DEFAULT_INT (l, 66) if (ARG_PRESENT (t)) ARG_DEFAULT_STR (t, ""s) else ARG_DEFAULT_STR (t, "Listing from prog"s) .ee Remember, default values are set [bf after] the call to 'parscl'! .pp [cc]mc | In the preceding example, we set the value of the argument [cc]mc for "l" to 66. This is simple enough. But for the "t" argument, we really have three different cases: the argument was specified with a string, the argument was specified without a string (meaning that we must use an empty title), or the argument was not specified at all (meaning that we use some other default). In the first case, neither call to ARG_DEFAULT_STR will do anything, since the string was specified by the user; in the second case, ARG_PRESENT (t) will be ".true." setting the default to the empty string (since the "t" argument was specified, even though it was without a string); and in the third case ARG_PRESENT (t) will be ".false.", setting the default to "Listing from prog". .pp Now that we have finished setting defaults, we can obtain the values of arguments with more macros: the call ARG_VALUE (l) will return the page length value: either the value specified by the user or the value 66 that we set as the default. ARG_TEXT (t) references an EOS-terminated string containing the title: either the value specified the user, an empty string, or "Listing from prog". Use of the values in our example might look like this: .be 6 page_len = ARG_VALUE (l) call ctoc (ARG_TEXT (t), title, MAXTITLE) if (ARG_PRESENT (p)) ### do printer formatting else ### do terminal formatting .ee And now, here's how all of the argument parsing will look: .be 19 include ARGUMENT_DEFS ARG_DECL PARSE_COMMAND_LINE ("l<req int> t<opt str> p<flag>"s, "prog [-l <page len>] [-t [<title]] [-p] {<file}>"s) ARG_DEFAULT_INT (l, 66) if (ARG_PRESENT (t)) ARG_DEFAULT_STR (t, ""s) else ARG_DEFAULT_STR (t, "Listing from prog"s) page_len = ARG_VALUE (l) call ctoc (ARG_TEXT (t), title, MAXTITLE) if (ARG_PRESENT (p)) ### do printer formatting else ### do terminal formatting .ee .pp Now, what about the file name arguments we were supposed to parse. Where did they go? 'Parscl' deletes arguments that it processes; it also ignores any arguments not starting with a hyphen (that do not appear after an letter-argument looking for a string). So the file name arguments are still there, ready to be fetched by 'getarg', with none of the "-t <title>" stuff left to confuse the logic of the rest of the program. .pp Now, how about some example commands to call this program: .be 20 prog -p (page_len = 66, title = "Listing from prog", formatted for printer) prog -l34 -t new title (page_len = 34, title = "new", file name = "title", formatted for terminal) prog file1 file2 -p -t -l70 (page_len = 70, title = "", file names = file1 file2, formatted for printer) prog filea -t"my new title" -l 60 (page_len = 60, title = "my new title", file name = filea, formatted for printer) prog -x filea (the "usage" message is printed) prog fileb -l (the "usage" message is printed) .ee As you can see, 'parscl' allows you to specify arguments in many different ways. For more information on 'parscl', see its entry in the Reference Manual. .SH "Dynamic Storage Management" Dynamic storage subroutines reserve and free variable size blocks from an area of memory. In this implementation, the area of memory is a one-dimensional array. Each block consists of consecutive words of that array. .pp The dynamic storage routines assume that you have included the following declaration in your main program and in any subprograms that reference dynamic storage: .be DS_DECL (mem, MEMSIZE) .ee where 'mem' is an array name that can be used to reference the dynamic storage area. You must also define MEMSIZE to an integer value between 6 and 32767 inclusive. This number is the maximum amount of space available for use by the dynamic storage routines. In estimating for the amount of dynamic storage required, you must allow for two extra 'overhead' words for each block allocated. Three other overhead words are required for a pointer to the first available block of memory and to store the value of MEMSIZE. .PH Dsinit The call .be call dsinit (MEMSIZE) .ee initializes the storage structure's pointers and sets up the list of free blocks. This call must be made before any other references to the dynamic storage area are made. .PH Dsget 'Dsget' allocates a block of words in the storage area and returns a pointer (array index) to the first useable word of the block. It takes one argument -- the size of the block to be allocated (in words). .pp After a call to 'dsget', you may then fill consecutive words in the 'mem' array beginning at the pointer returned by 'dsget' (up to the number of words you requested in the block) with whatever information called for by your application. If you should write more words to the block than you allocated, the next block will be overwritten. Needless to say, if this happens you may as well give up and start over. .pp If 'dsget' finds that there is not enough contiguous storage space to satisfy your request, it prints an error message, and if you desire, calls 'dsdump' to give you a dump of the contents of the dynamic storage array. .PH Dsfree A call to 'dsfree' with a pointer to a block of storage (obtained from a call to 'dsget') deallocates the block and makes it available for later use by 'dsget'. 'Dsfree' will warn you if it detects an attempt to free an unallocated block and give you the option of terminating or continuing the program. .PH Dsdump The dynamic storage routines cannot check for correct usage of dynamic storage. Because block sizes and pointers are also stored in 'mem' it is very easy for a mistake in your program to destroy this information. 'Dsdump' is a subroutine that can print the dynamic storage area in a semi-readable format to assist in debugging. It takes one argument: the constant LETTER for an alphanumeric dump, or the constant DIGIT for a numeric dump. .pp The following example shows the use of the dynamic storage routines and uses 'dsdump' to show the changes in storage that result from each call. .be 22 define (MEMSIZE, 35) pointer pos1, pos2 # pointer is a subsystem defined type pointer dsget DS_DECL (mem, MEMSIZE) call dsinit (MEMSIZE) call dsdump (LETTER) # first call pos1 = dsget (4) call scopy ("aaa"s, 1, mem, pos1) call dsdump (LETTER) # second call pos2 = dsget (3) call scopy ("bb"s, 1, mem, pos2) call dsdump (LETTER) # third call call dsfree (pos2) call dsdump (LETTER) # fourth call stop end .ee The first call to 'dsdump' (after 'init') produces the following dump: .be 4 * DYNAMIC STORAGE DUMP * 1 3 words in use 4 32 words available * END DUMP * .ee The first three words are used for overhead, and 32 (MEMSIZE - 3) words are available starting at word four in 'mem'. .pp The second call to 'dsdump' (after the first write to dynamic storage) produces the following: .be 6 * DYNAMIC STORAGE DUMP * 1 3 words in use 4 26 words available 30 6 words in use aaa * END DUMP * .ee Note that only four characters were written, three a's and an EOS (an EOS is a nonprinting character), but two extra control words are required for each block. That block is comprised of words 30 - 35 in the array 'mem'. .pp The third call to 'dsdump' (after the second 'scopy') produces the following: .be 8 * DYNAMIC STORAGE DUMP * 1 3 words in use 4 21 words available 25 5 words in use bb 30 6 words in use aaa * END DUMP * .ee The final call to 'dsdump' produces: .be 6 * DYNAMIC STORAGE DUMP * 1 3 words in use 4 26 words available 30 6 words in use aaa * END DUMP * .ee As you can see, the second block of storage that began at word 25 has been returned to the list of available space. .SH "Symbol Table Manipulation" Symbol table routines allow you to index tabular data with a character string rather than an integer subscript. For instance, in the following table, the information contained in "field1", "field2", and "field3" can obtained by specifying a certain key value (e.g. "firstentry"). .be 7 ---------------------------------------- |key |field1 | field2 |field3| ---------------------------------------- |firstentry | 10268 | data | u | | | | | | |secondentry | 27043 | moredata | a | ---------------------------------------- .ee .pp All Subsystem symbol table routines use dynamic storage. Therefore, the declarations and initialization required for dynamic storage are also required for the symbol table routines; namely: .be 3 DS_DECL (mem, MEMSIZE) ... call dsinit (MEMSIZE) .ee where 'mem' is an array name that can be used to reference the dynamic storage area, and MEMSIZE is a user-defined identifier describing how many words are to be reserved for items in dynamic storage. MEMSIZE must be a integer value between 6 and 32767 inclusive. For a discussion on how to estimate the amount of dynamic storage space needed in a program, you can refer back to the section on the dynamic storage routines. .pp A symbol table entry consists of two parts: an identifier and its associated data. The identifier is a variable length character string; it is dynamically created when the symbol is entered into a symbol table. The data associated with the symbol is treated as a fixed-length array of words to be stored or modified when the associated symbol is entered in the table and returned when the symbol is looked up. The size of the data is fixed for each symbol table -- each entry in a table must have associated data of the same size, but different symbol tables may have different lengths of data. .PH Mktabl A symbol table is created by a call to the pointer function 'mktabl' with a single integer argument giving the size of the associated data array or the "node size". 'Mktabl' returns a pointer to the symbol table in dynamic storage. This returned pointer identifies the symbol table -- you must pass it to the other symbol table routines to identify which table you want to reference. A symbol table is relatively small (each table requires about 50 words, not counting the symbols stored in it), so you may create as many of them as you like (as long as you have room for them). .pp In the table above, if "field1" and "field3" require one word each, and "field2" requires no more than 9 words, then you can create the symbol table with the following call: .be 3 pointer extable ... extable = mktabl (11) .ee The argument to 'mktabl' is 11 -- the total length of the data to be associated with each symbol. .PH Enter To enter a symbol in a symbol table, you must provide two items: an EOS-terminated string containing the identifier to be placed in the table, and an array containing the data to be associated with the symbol. Of course this array must be at least as large as the "nodesize" declared when the particular symbol table was created. A call to the subroutine 'enter' with the identifier, the data array, and the symbol table pointer will make an entry in the symbol table. However, if the identifier is already in the table, its associated data will be overwritten by that you've just supplied. It is not possible to have the same identifier in the same symbol table twice. .pp Now, continuing our example, to enter the first row of information in the table, you can use the following statements: .be 4 info (1) = 10268 call scopy ("data"s, 1, info, 2) info (11) = 'u'c call enter ("firstentry"s, info, extable) .ee .PH Lookup Once you've made an entry in the symbol table, you can retrieve it by supplying the identifier in an EOS-terminated string, an empty data array, and the symbol table pointer to the function 'lookup'. If 'lookup' can find the identifier in the table, it will fill in your data array with the data it has stored with the symbol and return with YES for its function value. Otherwise, it will just return with NO as its function value. .pp In our example, to access the data associated with the "firstentry" we can make the following call: .be foundit = lookup ("firstentry"s, info, extable) .ee After this call (assuming that "firstentry" was in the table), "foundit" would have the value YES, "info (1)" would have the value for "field1", "info (2)" through "info (10)" would have the value for "field2", and "info (11)" would have the value for "field3". .PH Delete If you should want to get rid of an entry in a symbol table, you can make a call to the subroutine 'delete' with identifier you want to delete in an EOS-terminated string and the symbol table pointer. If the identifier you pass is in the table, 'delete' will delete it and free its space for later use. If the identifier is not in the table, then 'delete' won't do anything. .pp Using our example again, if you want to delete 'firstentry' from the table, you can just make the call .be call delete ("firstentry"s, extable) .ee and "firstentry" will be removed from the table. .PH Rmtabl When you are through with a table and want to reclaim all of its storage space, you pass the table pointer to 'rmtabl'. 'Rmtabl' will delete all of the symbols in the table and release the storage space for the table itself. Of course, after you remove a table, you can never reference it again. .pp To complete our example, we can get rid of our symbol table by just calling 'rmtabl': .be call rmtabl (extable) .ee .PH Sctabl So far, the routines we've talked about have been sufficient for dealing with symbol tables. It turns out that there is one missing operation: getting entries from the table without knowing the identifiers. The need for this operation arises under many circumstances. Perhaps the most common is when we want to print out the contents of a symbol table for debugging. .pp To use 'sctabl' to return the contents of a symbol table, you first need to initialize a pointer with the value zero. We'll call this the position pointer from now on. Then you call 'sctable' repeatedly, passing it the symbol table pointer, a character array for the name, a data array for the associated data, and the position pointer. Each time you call it, 'sctabl' will return another entry in the table: it will fill in the character string with the entry's identifier, fill in your data array with the entry's data, and update position in the position pointer. When there are no more entries to return in the table, 'sctabl' returns EOF as its function value. .pp There are two things you have to watch when using 'sctabl'. First, if you don't keep calling 'sctabl' until it returns EOF, you must call 'dsfree' with the position pointer to release the space. Second, you may call 'enter' to .ul modify the value of a symbol while scanning a table, but you cannot use 'enter' to add a new symbol or use 'delete' to remove a symbol. If you do, 'sctabl' may lose its place and return garbage, or it may not return at all! .pp Here is a subroutine that will dump the contents of our example symbol table: .ne 19 .be # stdump --- print the contents of a symbol table subroutine stdump (table) pointer table integer posn integer sctabl character symbol (MAXSTR) untyped info (11) call print (ERROUT, "*4xSymbol*12xInfo*n"s) posn = 0 while (sctabl (table, symbol, info, posn) ~= EOF) call print (ERROUT, "*15s|*6i|*9s|*c*n"s, symbol, info (1), info (2), info (9)) return end .ee If make a call to 'stdump' after made the entry for "firstentry", it would print the following: .be Symbol Info firstentry | 10268|data |u .ee .SH "Other Routines" There are a number of miscellaneous routines that provide often needed assistance. The following table gives their names and a brief description. For full information on their use, see the Subsystem reference manual: .sp .in +14 .rm -5 .lt +5 .ta 10 .ti -9 date\Obtain date, time, process id, login name .sp .ti -9 error\Print an error message and terminate .sp .ti -9 follow\Follow a path and set the current and/or home directories .sp .ti -9 remark\Print a string followed by a newline .sp .ti -9 tquit$\Check if the break key was hit .sp .ti -9 wkday\Determine the day of the week of any date .in -14 .rm +5 .sp