A DESCRIPTION OF TRAC Copyright 1994 by John Cowan All rights reserved.

The TRAC programming language was designed and implemented by C. N. Mooers and L. P. Deutsch, and described in the following two articles: Mooers and Deutsch, "TRAC: A text handling language." 20th National Conference (1965) pp. 229-46 Proc. A.C.M.

Mooers. "TRAC, A procedure-describing language for the reactive typewriter." CACM 9:3 (1966) pp. 215-19. I have not read either of these articles. The information herein is drawn from the book >Macro Processors<, by A. J. Cole (Cambridge University Press, 1976, ISBN 0-521-29024-4). Disclaimer: At one time, TRAC was a trademark within the U.K. and possibly other countries. Given the rate of change in the software industry in the past thirty years, I doubt whether the trademark is still being defended in 1994. However, if so, it is hereby acknowledged. 1. A Brief Tour of TRAC TRAC is in essence a macro-processing language: like the well-known C preprocessor `cpp', it reads its input and passes through everything to its output, unchanged, except what is recognized as a macro call. Macro calls, which may have arguments, are evaluated and replaced by their results. For each macro, there are two ways to invoke it, active invocation and neutral invocation. Active invocation means that the result of the macro call is rescanned to see if it contains further macro calls. Neutral invocation means that the result is not rescanned but is sent to the output. Macro calls may be nested within macro calls, and active and neutral invocations may be freely intermixed. Further examples should help to make this distinction clearer. Syntactically, every TRAC macro invocation looks like: #(name,arg,arg,...) for an active invocation, or ##(name,arg,arg,...) for a neutral invocation. The ellipses (`...') are not part of the syntax, but are used to show that there may be any number of arguments, including zero. It is important to remember that in TRAC, an argument is a literal string of characters, which may of course contain calls on other macros. When the descriptions below speak of `the first argument' or the like, what is meant is literally the characters (after macro substitution) that appear after the first comma and up to the next comma or right parenthesis. Consider the following TRAC program: #(ps,#(rs))

Here the `ps' macro is invoked with a single argument. The effect of this macro is to print its argument on the standard output. So a first cut at what this program does is that it prints `#(rs)'. In fact, however, its argument contains another macro, which must be invoked first, the `rs' macro. The effect of `rs' is to read characters from the standard input up to an apostrophe (why an apostrophe? That's how TRAC works!) and return them as a string. A second cut at the meaning of this program, then, is that it is like `cat'; it reads the standard input (at least as far as the first apostrophe) and prints what it reads. This is correct as far as it goes, but note the active invocation of `rs'. What `rs' reads and returns is rescanned by TRAC, due to the active invocation, and any macros contained in it are invoked. So what we really have here is a TRAC interpreter written in TRAC! (In fact, the real TRAC interpreter loads this program initially and whenever it has nothing else to do; therefore, running TRAC has the effect of reading strings delimited by apostrophes from the standard input, invoking any macros, and sending the results to the standard output.) The true version of `cat' (up to the first apostrophe, at least) is: #(ps,##(rs)) The neutral invocation of `rs' prevents its result from being rescanned for macros, and so the input is simply printed. TRAC also allows input text to be quoted, to prevent expanding any macros in it. This is done by enclosing the text in parentheses. Everything from a `(' not preceded by `#' up to the matching ')' will not be scanned for macro invocations. (Of course, the text must be balanced with respect to parentheses.) To print the literal text `#(rs)', use the program: #(ps,(#(rs))) Any uses of `#' not followed by `(' or `#(', and random appearances of `,' and `)', are not treated as special. For convenience in typing, all tab and newline characters and all spaces appearing immediately after an argument-separating comma are removed by TRAC from the input stream. This does not apply to tabs, newlines, or spaces that are quoted within parentheses. The list of TRAC macros is fixed and cannot be extended. This is no real restriction, however, as the user may create objects called `forms' that serve the purposes of user-written macros in languages like `cpp'. Forms are defined with the `ds' and `ss' macros, and invoked (possibly with arguments) using the `cl' macro. TRAC's two-letter macro names descend from the days of slow TTYs; the above names stand for `define string', `segment string', and `call' respectively. Forms may have names of any length, even including zero (a form with null name is often useful for error recovery). 2. The Original TRAC Macros Here is a complete list of all the TRAC macros. The two-letter form shown first is the standard Mooers/Deutsch name; the longer form shown second is also accepted by the Perl implementation, and is intended to

enhance the readability of TRAC programs. In retrocomputing implementations, backward compatibility is a laudable goal, but so is adaptation to the modern world, insofar as the spirit of the language is preserved. As a matter of convention, all macros are shown in active-invocation form, but a neutral-invocation form (with `##' instead of `#') also exists. The following descriptions carefully differentiate between the `result' of a macro, which is the string it returns, and the `effect' of a macro, which is anything else it does other than returning a result. For example, `ps' has the effect of printing its argument on the standard output, but its result is always the null string. Note that TRAC string. Extra expanded (with Unknown macros 2.1. Basic I/O The following macros provide basic TRAC string I/O, and have been partly explained above. See Section 3 for some extensions made in the Perl implementation. #(rs) #(read string) Reads characters from the standard input up to and including the current system meta character (see `cm' below). The meta character is discarded, and all other characters are concatenated into a string and returned. At end of file, the null string is returned (but this is not distinguishable from the null string resulting from no characters preceding the meta character). #(ps,any text) #(print string,any text) Prints its first argument on the standard output. null string. #(rc) #(read character) Reads a single character from the standard input and returns it. When reading from a terminal, this macro does not put the terminal into character-at-a-time mode. The meta character may be returned like any other. At end-of-file, the result is the null string. #(cm,X) #(change meta,X) Changes the meta character to be the first character of the first argument. The remaining characters of the first argument are ignored. The result is the previous meta character (this is an extension peculiar to the Perl implementation; the traditional implementation returned the null string). This result allows saving and restoring the meta character. A common value for the meta character is the newline character. The result is always the interpreters always treat missing arguments as the null arguments may be supplied, and any macros in them will be possible effects), but the values are ignored. are ignored; the result is the null string.

2.2 Forms And The Form Store The following macros serve to manipulate a data structure internal to the TRAC interpreter, known as the `form store'. Essentially, this is just a list of strings called `forms' each of which has a unique name (another string). However, forms may contain some special codes which are not characters. (In the Perl implementation, these are represented by non-ASCII characters in the range 0200-0377; so Perl TRAC is not `8-bit clean'.) In addition, each form has a pointer associated with it, called the `form pointer', which can point to any character in the form. This is used to manipulate parts of forms using some of the macros explained below. #(ds,name,text) #(define string,name,text) Causes the second argument to become a form in the form store with a name given by the first argument. If the second argument contains macro calls which are not to be evaluated now (at definition time), then the whole argument should be protected by parentheses. If there is an existing form in the form store with the same name, it is quietly removed. This characteristic of quietly removing old versions of things is maintained throughout TRAC. Note particularly that the null string is a valid form name. The result of `ds' is always the null string. #(ss,name,arg1,arg2,...) #(segment string,name,arg1,arg2,...) As in Section 1, the ellipsis is not part of the syntax, but indicates that the `ss' macro may have any number of arguments from one on up. This macro alters the existing form named `name' (if no such form exists, nothing is done) by scanning it for all instances of the second argument and replacing every instance with a non-character which is externally represented (by the `sb' and `pf' macros) as `\0\'. The form is then scanned again for all instances of the third argument, which are replaced by the non-character represented as '\1\'. This process is repeated until there are no more arguments to `ss'. The result is always the null string. The `ds' and `ss' macros are used jointly to define the equivalent of user-written macros in TRAC. The user writes a piece of TRAC code using `ds', employing any desired strings as formal parameters. The `ss' macro then converts the user-chosen strings into internal markers. See Section 5 for further examples. #(cl,name,arg1,arg2,...) #(call,name,arg1,arg2,...) The result is the form named by the first argument. Any markers in the form are replaced by the second through last arguments. If an argument is not provided, the corresponding marker is replaced by the null string, so that no markers are ever returned from `cl'. This is the mechanism for invoking user-written macros defined by `ds' and `ss'. #(dd,name1,name2,...) #(delete definition,name1,name2) Deletes any forms from the form store whose names are given as arguments.

Arguments not corresponding to names are silently ignored, as usual. Returns the null string. #(da) #(delete all) Deletes all forms from the form store. #(cs,name,default) #(call segment,name,default) A `segment' is defined as the portion of a form between markers. The `cs' macro returns the segment currently pointed to by the form pointer. If the form pointer is not pointing to the beginning of a segment, it returns all the characters between the form pointer and the next marker. This macro is the first to have a `default' argument. All default arguments are handled in the same way. When an exceptional event occurs, the default argument is returned as the result of the macro, and the macro is treated as if it had been invoked actively, so that the default argument will be rescanned for macro calls. In the case of `cs', the default argument is returned as the result when there are no more segments to return. #(cc,name,default) #(call character,name,default) The `cc' macro is very similar to the `cs' macro, but returns the character indicated by the form pointer, and leaves the form pointer pointing to the next character of the form. The default argument is returned if there are no more characters, or if the next character is a marker. #(in,name,string,default) #(initial,name,string,default) This macro scans the form named by the first argument, starting at the form pointer for that form, to try to find the string which is the second argument. If it is found, the form pointer is left pointing after the matched string, and the null string is returned. If not, the default argument is returned. #(cr,name) #(call reset,name) Resets the form pointer associated with the form whose name is the first argument to the beginning of the form. Returns the null string. 2.3. TRAC Arithmetic TRAC has simple built-in arithmetic macros. Most of these accept TRAC numbers as arguments. A TRAC number is a string with a particular form: an optional sign, plus or minus; followed by any number of non-digits, called the `prefix', followed by any number of digits. If there are no digits, the value is zero. (The traditional implementation allowed digits in the prefix as long as they were followed by at least one non-digit.) TRAC numbers are always interpreted as signed integers. The prefix is ignored for computational purposes, but the prefix of the result is the same as the prefix of the first numeric argument. (The prefix of the second argument, if any, is Returns the null string.

always discarded.) #(ad,n1,n2,default) #(+,n1,n2,default) Returns the sum of the first two arguments, unless overflow results, in which case the default argument is returned under the usual rules. #(su,n1,n2,default) #(-,n1,n2,default) Returns the first argument minus the second argument, unless overflow results, in which case the default argument is returned. #(ml,n1,n2,default) #(*,n1,n2,default) Returns the product of the first two arguments, unless overflow results, in which case the default argument is returned. #(dv,n1,n2,default) #(/,n1,n2,default) Returns the first argument divided by the second argument, with any remainder discarded (truncation is toward zero), unless overflow results, or the second argument is zero, in which case the default argument is returned. #(eq,string1,string2,then,else) #(=,string1,string2,then,else) Compares the first two arguments for character-by-character equality. If they are equal, the third argument is returned; otherwise, the fourth argument is returned. This macro looks weak until it is remembered that the third and fourth arguments may contain macro calls protected by parentheses, which will then be rescanned on return. #(gr,string1,string2,then,else) #(<,string1,string2,then,else) Compares the first two arguments, which should be numeric, for numerical magnitude. If the first argument is greater than the second argument, the third argument is returned; otherwise, the fourth argument is returned. #(bu,n1,n2) #(|,n1,n2) Returns the bitwise boolean union (logical OR) of the first two arguments considered as numbers. #(bi,n1,n2) #(&,n1,n2) Returns the bitwise boolean intersection (logical AND) of the first two arguments considered as numbers. #(bc,n1) #(~,n1)

Returns the bitwise boolean complement (logical NOT) of the first argument, considered as a number. 2.4. Miscellaneous Macros #(tn) #(trace on) Causes TRAC to print the name and arguments of each macro just before it is invoked, and wait for user input. A newline will proceed; the interrupt character (typically DEL or ^C) will abort execution and return to the main TRAC interpreter loop. The result is the null string. #(tf) #(trace off) Turns off tracing mode. #(ln) #(list names) Returns a list, separated by spaces, of all the names of the forms in the form store as the result. #(pf,name) #(print form,name) Prints the form named by the first argument on the standard output. Markers are printed using the `\n\' format, where `n' is a sequence of digits. Actual backslashes are printed as `\\'. #(sb,pathname,name1,name2,...) #(save block,pathname,name1,name2,...) Saves the forms named by the second and succeeding arguments into the file specified by the first argument. The syntax of a filename depends on the local environment. (As an extension, the Perl implementation tags each form saved with the pathname; a later `sb' invocation with a single argument saves the same forms that were saved previously.) Nonexistent form names are quietly skipped. The format of the file is as follows: a form name (with any backslashes printed as `\\'), followed by '\=', followed by the form (as printed by the `pf' macro), followed by '\;' and a newline. Either the name or the form itself may contain newlines, so the `\=' and '\;' are critical; the newline after '\;' helps conventional text editors. #(fb,pathname) #(fetch block,pathname) Reads the file named by the first argument, which should be in the format produced by `sb', into the form store, eliminating any similarly named forms already present there. (As an extension, the Perl implementation tags each form read in with the filename; these forms can then be saved with a `sb' having just one argument, the same filename.) The result is the null string. The result is the null string.

#(eb,filename) #(erase block,filename) Erases the file named by the first argument. This may be any file, not just one written by `sb', although that is the intended use. 3. Extensions In The Perl Implementation The Perl implementation of TRAC adds a few new macros, and a few extra arguments to traditional macros, to aid the use of TRAC in modern (i.e. Unix-like) environments. The `default' argument has the usual meaning. #(oi,handle,filename,default) #(open input,handle,filename,default) Opens the file named by the second argument for input, associating with it the handle (any string) given by the first argument. There is a system-defined limit on how many handles may be open simultaneously. The handle STDIN refers to the standard input. The handles `' (the null string) and `ARGV' should be avoided due to conflicts with Perl. The result is the null string, unless the file cannot be opened, in which case the result is the default argument (with the usual side effect of making the invocation an active one). #(oo,handle,filename,default) #(open output,handle,filename,default) Opens the file named by the second argument for output (creating it if it does not exist, or truncating it to zero length if it does), associating it with the handle (any string) given by the first argument. There is a system-defined limit on how many handles may be open simultaneously. The handles STDOUT and STDERR refer to the standard output and the standard error respectively. The handles `' (the null string) and `ARGV' should be avoided due to conflicts with Perl. The result is the null string, unless the file cannot be opened, in which case the result is the default argument (with the usual side effect of making the invocation an active one). #(oa,handle,filename,default) #(open append,handle,filename,default) Exactly the same as `oo', except that if the file exists, its contents are appended to rather than overwritten. #(cf,handle) #(close file,handle) Closes the file named by the first argument. #(rs,handle,default) #(read string,handle,default) If the traditional `rs' macro is given an argument, it is taken to be the name of a handle to read from, rather than reading from the standard input. A second argument is a default argument, returned on end of file. #(rc,handle) The result is the null string.

#(read character,handle) If the traditional `rc' macro is given an argument, it is taken to be the name of a handle to read from, rather than reading from the standard input. #(cm,X,handle) #(change meta,X,handle) There is a separate meta character associated with each input handle. Specifying a second argument to the traditional `cm' macro changes the meta character for that handle and returns the meta character associated with the handle. Initially, the meta character for STDIN is the apostrophe. For all other handles, the currently set STDIN meta character is copied when the handle is opened. #(ps,any text,handle) #(print string,any text,handle) If the traditional `ps' macro is given a second argument, it is taken to be an output handle to print to, rather than using the standard output. #(ld,filename,default) #(load,filename,default) Returns the contents of the entire file whose name is specified by the first argument. The file will be loaded into memory first, so beware! This is mostly useful for loading files containing TRAC code. If the file cannot be opened or cannot be read, the default argument is returned instead. #(sy,any text) #(system,any text) Causes the first argument to be passed to the system for execution. The result is the 16-bit value passed back by the wait(2) system call, expressed as a TRAC number. #(ex) #(exit) Causes an immediate exit from TRAC. #(#,any text) This macro has null result and null effect, and can be used to insert comments (properly balanced as to parentheses, of course). Finally, there are two special forms in the form store, named ARGV and ENV. The ARGV form contains the arguments passed when TRAC is invoked, separated by the marker `\0\'. The ENV form contains the environment as name-value pairs; each pair is separated by the marker `\0\', and each name is separated from its following value by the marker `\1\'. The ENV form is not in any predictable order. These forms may be manipulated freely like any other forms. 4. An Example: Towers Of Hanoi No result is returned.

This example, found on pp. 72-4 of Cole (see Section 1), solves the Towers of Hanoi problem: see any elementary programming text for an explanation of the problem and a typical recursive solution. #(ds,the other,(##(su,6,##(ad,this,that)))) This creates a form named `the other' with contents `##(su,6,##(ad,this,that))'. Note that the second-from-outermost parentheses protect the `su' and `ad' macro calls from invocation at this time. #(ss,the other,this,that) This segments the form named `the other', replacing `this' and `that' with the markers `\0\' and `\1\' respectively. When invoked below, `the other' will accept two numeric arguments representing spindles (which are numbered 1, 2, 3) and return the number of the other spindle. #(ds,hanoi,(#(gr,N,1, (#(cl,hanoi,this,#(cl,the other,this,that),##(su,N,1)) #(ps,from this to that( #(cl,hanoi,#(cl,the other,this,that),that,##(su,N,1))), (#(ps,from this to that( ))) #(ss,hanoi,this,that,N)

)) )))

This code defines `hanoi' as a segmented form, with parameters `this' and `that' (the spindle numbers to move from and to, respectively) and `N', the number of disks to move. The body is a (protected) invocation of the `gr' macro. If the number of disks is greater than 1, move N - 1 disks from `this' to the other (using the form `the other' to compute its spindle number), move one disk from `this' to 'that', and then move N - 1 disks from the other to `that'. If N is 1, just move the one disk directly. The `ps' calls print the moves made. Note the quoted newline at the end of each `ps' call.