A FORTRAN->LISP Translator This paper appears in the

Proceedings of the 1979 Macsyma Users' Conference,

Washington, D.C., June 20-22, 1979. The substance of the original text appears in normal fonting, though a small number of out-and-out typos were corrected. Oddities of spelling that were the custom of the time (either generally, or just for me) were left alone. Some formatting of headings and tables was adjusted slightly for HTML. Any new text that has been added appears bracketed and in color green; such text is intended to help clarify the historical context, since considerable time passed between the time this paper was published and the time I converted it to HTML.

--Kent Pitman, 17-Oct-1998. Annotated original document follows.

Click here for an index of other titles by Kent Pitman.

A FORTRAN->LISP Translator

Kent M. Pitman

[address at time of publication]

MIT Laboratory for Computer Science

545 Technology Square

Cambridge, MA 02139

I. Introduction

The development of the Fortran->Lisp translator was intended to serve two primary needs within the Macsyma [1] system. Its immediate goal was to provide the user with the ability to call subroutines from commercially available software libraries (such as IMSL [2]) directly from the Macsyma system. In addition, since many of our users have their own numerical libraries written in Fortran, it was designed to offer a more direct interface between such libraries and Macsyma.

The translator is intended to serve as much more than just a black-box interface between the worlds of Fortran and MacLISP [3], however. The lisp code output by the translator is expected to be readable and maintainable by a lisp programmer, while still preserving the basic structure of the source Fortran.

This paper deals in general terms with the issues which have influenced the design of the translator. It introduces the concept of supporting a Fortran virtual machine from within MacLISP, illustrates how many basic Fortran concepts are represented in the translated code and the runtime environment, and concludes with a description of how the end product is likely to interface with Macsyma.

II. Overview

In broad terms, the process of translation may be viewed as a sequence of transformations between the following stages. This section will be devoted to a brief summary of the major states through which the code passes.

Fortran Source. The Fortran source file contains the routines to be translated. Wherever possible, we have sought to cater to both the ANSI Fortran standard and to any commonly observed extensions on input, in an effort to gain maximum compatibility with varying dialects of Fortran. `Free format' label fields are permitted when they do not conflict with legal configurations specified by the standard. (See Figure 1(a).) Translator Internal Representation. The Fortran source is read by the first pass of the translator, and parsed into an internal form composed of lists of tokens arranged in a hierarchical structure representing the Fortran semantic tree [4]. Translator Output. The output of the translator is a macro form in a syntax very close to that of Lisp. The functions called are for the most part not lisp primitives, but rather macro forms which will be expanded into Lisp in the MacLISP compiler [5]. Code in this state will be referred to throughout this paper as Fortran macro code. (See Figure 1(b).) By this point in the translation, all implicit declarations have been made explicit and must remain so from this point on. Arbitrary pieces of straight Lisp code may be inserted by the user into the macro form at this point without breaking the translation. Additionally, macro code may be deleted or added to the extent of creating or modifying the declarations of variables provided certain conventions are adhered to. The translator output has the feature of visually resembling both Fortran and Lisp in a way that we hope will be readable by programmers of either language with a minimal amount of difficulty. As much as possible, variable names have not been changed, and the original control structure should be visible despite the transformations that have been made during the translation up until this point. Some experiments were done with outputting straight Lisp code; however, this code was very long, dense, and generally unreadable. The use of macros makes the uncompiled output file considerably smaller (about half the size required for fully macro expanded code) and much easier to read and edit. Macro Expansion. Macro expansion happens either in the compiler or, if the code is being interpreted, at EVAL time It is at this time that the macro forms output by the translator are expanded into their actual lisp equivalents. (See Figure 1(c).)

SUBROUTINE MATMUL (X, Y, IDIM) REAL X(IDIM,1),Y(IDIM,1),Z(100) DO 100 I=1,IDIM Z(I)=0. DO 200 J=1,IDIM 200 Z(I)=X(I,J)*Y(J,I)+Z(I) DO 100 J=1,IDIM 100 X(I)=Z(I) RETURN END Figure 1(a). Fortran Source

(FORTRAN (MATMUL X Y IDIM) (SUBROUTINE (REAL (X 1) (Y 1) (Z 100)) (INTEGER IDIM I J)) (: I 1) DO-I-100 (: (Z I) 0.0) (: J 1) DO-J-200 /200 (: (Z I) (+$ (Z I) (*$ (X I J) (Y J I)))) (COND ((<= (: J (1+ J)) IDIM) (GO DO-J-200))) DO-J-100 (: (X I) (Z I)) (COND ((<= (: J (1+ J)) IDIM) (GO DO-J-100))) (COND ((<= (: I (1+ I)) IDIM) (GO DO-I-100))) (RETURN T)) Figure 1(b). Translator Output (Macro Code)

(PROGN 'COMPILE (DECLARE (NOTYPE (MATMUL FIXNUM FIXNUM FIXNUM))) (FORTRAN$ALLOCATE 102.) ;Allocate Z(0:99), I, J (DEFUN MATMUL (X Y IDIM) (PROG NIL (STORE (ARRAYCALL FIXNUM FORTRAN$ARRAY 100.) 1.) DO-I-100 (STORE (ARRAYCALL FLONUM FORTRAN$ARRAY (ARRAYCALL FIXNUM FORTRAN$ARRAY 100.)) 0.0) (STORE (ARRAYCALL FIXNUM FORTRAN$ARRAY 101.) 1.) DO-J-200 /200 (STORE (ARRAYCALL FLONUM FORTRAN$ARRAY (ARRAYCALL FIXNUM FORTRAN$ARRAY 100.)) (+$ (ARRAYCALL FLONUM FORTRAN$ARRAY (ARRAYCALL FIXNUM FORTRAN$ARRAY 100.)) (*$ (ARRAYCALL FLONUM FORTRAN$ARRAY (+ X (ARRAYCALL FIXNUM FORTRAN$ARRAY 100.))) (ARRAYCALL FLONUM FORTRAN$ARRAY (+ Y (ARRAYCALL FIXNUM FORTRAN$ARRAY 100.)))))) (COND ((NOT (GREATERP (STORE (ARRAYCALL FIXNUM FORTRAN$ARRAY 101.) (1+ (ARRAYCALL FIXNUM FORTRAN$ARRAY 101.))) (ARRAYCALL FIXNUM FORTRAN$ARRAY IDIM))) (GO DO-J-200))) (STORE (ARRAYCALL FIXNUM FORTRAN$ARRAY 101.) 1.) DO-J-100 /100 (STORE (ARRAYCALL FLONUM FORTRAN$ARRAY (+ X (ARRAYCALL FIXNUM FORTRAN$ARRAY 100.))) (ARRAYCALL FLONUM FORTRAN$ARRAY (ARRAYCALL FIXNUM FORTRAN$ARRAY 100.))) (COND ((NOT (GREATERP (STORE (ARRAYCALL FIXNUM FORTRAN$ARRAY 101.) (1+ (ARRAYCALL FIXNUM FORTRAN$ARRAY 101.))) (ARRAYCALL FIXNUM FORTRAN$ARRAY IDIM))) (GO DO-J-100))) (COND ((NOT (GREATERP (STORE (ARRAYCALL FIXNUM FORTRAN$ARRAY 100.) (1+ (ARRAYCALL FIXNUM FORTRAN$ARRAY 100.))) (ARRAYCALL FIXNUM FORTRAN$ARRAY IDIM))) (GO DO-I-100))) (RETURN T)))) Figure 1(c). Macro Expansion

III. Design Considerations

The Fortran Virtual Machine

MacLISP and Fortran are almost completely incompatible when it comes to such issues as argument passing and side-effects. Each has a completely different philosophy on the subject. It is, however, possible to emulate the workings of a Fortran Virtual Machine in MacLISP by an appropriate choice of representation.

MacLISP's function calling sequence is call by value; most Fortran implementations use call by reference. Because a Lisp-called function has no pointer to the storage slot represented by the actual parameter, it is difficult for it to side-effect on the actual parameter in a straightforward manner. To solve this problem, all translated programs store their data in a single Lisp FIXNUM array [6] [7] which is globally accessible at runtime. The objects which are passed between programs are not variable values, but rather integer offsets into the data array (analogous to the machine pointers to data locations that would be passed in an actual Fortran subroutine call). Expressions which are used as actual parameters to a Fortran routine are evaluated at runtime and stored in temporary slots in the array; offset pointers locating the temporary slots are passed to the called routine.

Relocatable data

A problem exists in generating compiler output which will run efficiently when loaded into a Lisp: Since all translated Fortran programs share the same array space, there is no way to know at compile time where in the array each program will go. Any number of other Fortran packages might have been loaded by the time a given one is being loaded. The only way at compile time, then, to reference a given variable is as an offset from the head of the data space belonging to that program unit, as in:

Location(<var>) = Location(<program>) + Offset(<var>).

Having to compute this value at runtime would be reasonably time consuming.

Problems of handling relocatable code occur in many versions of Fortran, and are generally handled by a loader which takes a program with relative addressing of the sort described above and turns them into an absolute machine addresses at load time.

MacLISP provides a general facility for handling this type of problem in the loader. The feature involves a special object called SQUID (Self QUoting Internal Datum) which is known to the compiler and the MacLISP Fasload package [8]. Specifically, this facility allows a user to specify forms which are to be evaluated at load time and replaced inline with their evaluated forms.

Using the SQUID facility, code can be output which references offsets from the beginning of a program's data space, a constant which can be computed at load time. At load time, all such references which are done as SQUID forms are evaluated and return FIXNUM s, which replace the expressions in the binary code.

Because of this optimization, it is necessary to add two additional stages in the transormation of the data, in addition to those mentioned earlier.

Compiler Output. The compiler will output a binary file (FASL file) which can be loaded by the Lisp. This binary file is not yet the final form of the translation, however, since it contains references will be expanded at the time the file is loaded into a Lisp. Runtime Environment and Loaded Routines. This is the final state of the world. The data will be effectively have gone through a relocation process similar to that done by a Fortran loader. The only changes which will occur to the dataspace and program beyond this point are those side-effects caused by storing and retrieving values during the normal run of a user program, and any allocation of extra space in the Fortran data-array for routines which were not translated Fortran routines, but which must communicate with translated Fortran routines.

It should be pointed out that there is an inherent inefficiency in translated code that will cause it to run several times slower than the Fortran would have. The mechanism for recalling a value from a location in the virtual machine involves several real-machine memory references. The reason is that MacLISP arrays may be relocated by the garbage collector, and so cannot be referenced as offsets from a fixed machine location. Instead they must be referenced as offsets from a pointer to the head of the array. (A sample piece of code and how it compiles is illustrated in Figure 2.)

The translator, however, does not propose to solve this inefficiency. Greater efficiency could be obtained by allocating a fixed area in the Lisp, immune to garbage collection, in which to store data associated with translated Fortran routines. By doing so, however, several things would be lost. For instance, such a data area could not be later reclaimed by the garbage collector for re-use by normal Lisp routines. Additionally, the mechanisms for storing and retrieving data from such an area would probably be very dependent on how the Lisp was implemented. Since the intent of the design is that the virtual machine be fully describable in Lisp, such means of achieving optimization were deemed inappropriate.

By remaining within the Lisp formalism, several things are gained, including the ability to do runtime debugging, and a certain immunity from the implementational details of the Lisp. Additionally, it is assumed that optimization is the function of a compiler, and that since a Lisp compiler should be available, it is redundant for the translator to do complex optimizations. (As can be seen in Figure 2, however, this assumption may also have its price. The code generated by the current compiler in this case is not optimal.)

Given local array A and formal parameter N, in the expression M(N)=N+5 the current Lisp compiler will generate code looking like this: MOVE AR2A,FARRAY ; Move a pointer to FARRAY into AR2A MOVE TT,(A) ; Move N into TT MOVE TT,@TTSAR(AR2A) ; Move FARRAY[N] into TT ADDI TT,5 ; Put FARRAY[N]+5 into TT MOVE D,TT ; Copy FARRAY[N]+5 into D MOVE TT,(A) ; Move N into TT MOVE TT,@TTSAR(AR2A) ; Move FARRAY[N] into TT ADDI TT,MIDX ; Add location of M into TT (TT now has FARRAY[M[N]]) MOVEM D,@TTSAR(AR2A) ; Store value in FARRAY[M[N]] For the same operation, a DEC Fortran compiler generated the following code: MOVE AC1,N ; Move N into AC1 MOVEI AC2,5 ; Move the consant 5 into AC2 ADD AC2,N ; Add N into AC2 (AC2 now contains N+5) MOVEM AC2,M-1(AC1) ; Store value in M(N) Figure 2. How translated Fortran compiles.

Runtime Environment

The runtime environment for a translated Fortran program consists of another library of routines that are too large to be macro-expanded at compile time, such as Input/Ouptut (I/O) handlers, as well as some Lisp programs which simulate the effect of a Fortran loader, allocating space for program-data and creating an interface between the translated Fortran programs and normal Lisp routines which may attempt to call them.

Data storage is allocated in the Lisp runtime environment as a single contiguous number array which all programs may access. All memory references are compiled as offsets in the single Fortran storage array, and expanded to absolute references at load time. (See figure 3.)

At some point in the loading of any translated Fortran program into a Lisp, the following events will occur: If the Fortran run-time support subroutine library is not already loaded, it is automatically loaded. If it does not exist, the array FORTRAN$ARRAY will be created. If it does exist, it will be extended by a function call requesting enough space in the array for main program data. If the program requires COMMON space, one of two things will happen. If space for a COMMON block by the same name has already been allocated, a pointer to that space will be returned. If such space has not been allocated, space will be allocated in FORTRAN$ARRAY for the block. The program will be loaded into the normal binary program space. At this time, addresses of data in the Fortran virtual memory area will be converted from relative to absolute offsets from the head of FORTRAN$ARRAY . Figure 3. Loading translated programs into Lisp

Common

COMMON blocks are processed in the translator in much the same way as ordinary program data. Rather than being transformed into offsets from a particular program data area, however, COMMON variables are referred to in the compiler output as offsets from the appropriate COMMON block area.

When a program referencing a COMMON block is loaded, it checks to see whether space for that block has already been allocated. If it has, pointers are established to the already existing block. If no such allocation has been made, a new block of the desired size is created and pointers are made to this newly created data area.

One problem introduced by this implementation of COMMON is that no COMMON block (including blank COMMON ) may be extended in size after its initial allocation. Most Fortran implementations allow blank COMMON to be extendable. On the other hand, such a feat is generally accomplished via obscure devices such as re-using the space taken up by the Fortran loader for blank COMMON , thereby making it impossible for DATA statements to be used on variables in blank COMMON . No really satisfactory solution to this problem has been suggested which does not entail a great loss in runtime efficiency.

Equivalences

It is a `feature' of LISP that two symbolic names cannot share the same storage; furthermore, it is not possible to have two arrays that share with each other in the way that Fortran will allow. Thus, it is not immediately obvious what code to output as the MacLISP equivalent to the Fortran EQUIVALENCE statement.

[Note: Common Lisp later added displaced arrays, so that two arrays could share with each other. Displaced arrays did not exist in MacLISP. Displaced arrays wouldn't have solved the problem of `type overlap' though; that is, of equivalencing a FLONUM to a FIXNUM .]

There is a genreal solution to the EQUIVALENCE problem which can be used in any language, regardless of whether they allow multiple names for a storage location or not. This solution is merely to select one of the names as the name that will be used, and change all the other EQUIVALENCE 'd names to that single name, making adjustments to subscripts as necessary. (Figure 4 shows an example of this process.)

PROGRAM MAIN EQUIVALENCE (A(10),B(0)) REAL A(20),B(10) DO 100 I = 1,10 B(I)=0 100 A(I)=B(I) Figure 4(a). Source program using EQUIVALENCE statement. PROGRAM MAIN REAL EQVAB(20) C A <- EQVAB( 1:20), B <- EQVAB(11:20) DO 100 I=1,10 EQVAB(I+10)=0 100 EQVAB(I)=EQVAB(I+10) Figure 4(b). General solution to the EQUIVALENCE problem.

Because of our self-imposed constraint of maintaining a translation that bears as much visual resemblance to the original Fortran as possible, the translator does not do EQUIVALENCE processing at all. Instead, EQUIVALENCE 's are processed at macro-expansion time. At that time, since all memory references are converted to numerical offsets into the Fortran virtual memory, it presents no problem to assign symbolic names the same numeric offset. (The algorithm used to set up these EQUIVALENCE 's is described in Figure 5.)

EQUIVALENCE <Equivalence-Set 1 >, <Equivalence-Set 2 >, ... <Equivalence-Set> :: ( Element 1 [Offset 1 ], Element 2 [Offset 2 ], ...) 1. Convert all scalars to single element array references. 2. Merge interrelated EQUIVALENCE sets. For any element, E[M] , in any EQUIVALENCE set, S , if an element with the same variable name occurs as E'[n] in some other EQUIVALENCE set, S' , then merge the two sets by subtracting the quantity (m-n) from all subscripts in set S and taking the union of the resulting list with the set S' . 3. Sort EQUIVALENCE sets. Sort EQUIVALENCE sets in order of ascending offset. 4. Normalize EQUIVALENCE sets which do not intersect with COMMON . For each EQUIVALENCE set with first element E[m] and which is free of variables declared members of a COMMON block, subtract the quantity (m-1) from all elements of the set (thus normalizing relative to the first element). 5. Normalize EQUIVALENCE sets which intersect with COMMON . Fore each EQUIVALENCE set which contains at least one element, E[m] , which is declared COMMON , subtract the quantity (m-1) from all elements of the set (thus normalizing relative to the COMMON element). Since COMMON blocks will have space allocated for them in the course of normal storage allocation, all that remains for EQUIVALENCE sets with elements in a COMMON block is to assign an alias to each of the elements of the set as offsets from the element that is in COMMON . 6. Determine memory required for EQUIVALENCE sets which don't involve COMMON . For an EQUIVALENCE set, S , with elements E 1 ,...,E n , which has no members which are declared COMMON , the actual amount of memory needed can be gotten via the computation, length(S) = max[size(E 1 ) + offset(E 1 ), size(E 2 ) + offset(E 2 ), ... ], where size(E k ) is a function of the DIMENSION 'd length and the number of machine words required for each virtual cell in the datatype of E k (e.g., the COMPLEX datatype requires 2 words per virtual cell). Having calculated and allocated the right amount of space for identifiers belonging to EQUIVALENCE sets, all that remains is to assign aliases to each of the elements of each set as offsets from the beginning of the set. Figure 5. How EQUIVALENCE is processed by the Translator.

IV. Storage Allocation for Variables

Arrays vs. Lists

The essential choice of data representations was between lists and arrays. Both formalisms are sufficiently powerful to adequately represent the effects of a Fortran virtual machine. If lists were chosen, assignment could be done via the RPLACA operation and the contents of a cell retrieved via CAR or NTH . If arrays were used, standard operations for storing and recalling array elements already built into MacLISP could be used.

Several factors were taken into consderation in the decision. Arrays have the feature of being randomly accessible in a reasonably efficient way. Most Fortran programs have been written on this assumption, and so transformations to a list could cause a considerable slowdown in operation. MacLISP's FIXNUM arrays are approximately twice as space efficient as lists -- another good reason for using arrays. Passing around pointers to lists when doing subroutine calling would be considerably easier than passing numeric offsets into the Fortran array, but not sufficiently so to justify the loss in space/time efficiency brought on by other factors.

How Many Arrays?

Given that an array strategy is the best for the Fortran data, there is still a question to answer about the implementation: Should every program have its own personal array, or should all programs share a large common workspace with all other programs? If each program had its own private data space, then subroutines would have to receive more than just array indexes as arguments. It would be necessary to pass twice as many arguments to each routine (to handle array pointers and offsets into the arrays separately), or to pass a cons of the form (<array-object> . <offset>) . Decoding such an object on every subroutine call is a lot of overhead if there is a way to avoid it. The same problem would occur even if just two arrays were used -- one for COMMON and one for normal program space. Having one big array means that when a new package is loaded, a large array copy must be done to incraese the array size appropriately; however, such operations are also not very frequent, so the benefits outweigh the costs.

Aliasing

In a translated Fortran program, just as in an actual Fortran implementation, variable names are merely conventions for speaking about a certain point in a large memory array. The names of variables are removed by the compilation phase and replaced by numerical pointers to storage addresses.

The processing of names is done by the macro package through a process called aliasing. Since the translator output code has already had any Fortran implicit declarations made explicit, the driver for the macro package is able to map over the declarations, creating for each Fortran identifier an `alias' to be used in the compiler output.

For example, a Fortran function statement such as:

F(X)=X**2+3

would generate an alias for F which looks like:

(LAMBDA (X) (+$ (^$ X 2) 3.0))

Normal variable references generate aliases which are offsets into FORTRAN$ARRAY . For example, the statement

EQUIVALENCE (A(2),B(1))

generates aliases for A and B of A[1] and A[0] , respectively. At a later point in the macro-expansion references such as A[1] are further expanded into ARRAYCALL 's to a numeric offset into the Fortran dataspace, thus eliminating all references to the symbolic name in the output code. (A separate symbol table may be generated for use in debugging the Fortran program.)

Variables encountered in the program which do not have an alias assigned to them will be left alone. This allows a user to insert arbitrary pieces of normal MacLISP code into the Fortran macro code without upsetting the translation.

Runtime I/O Library

Since MacLISP and Fortran I/O routines are very different from one another, open coding of FORTRAN formatted READ/WRITE operations is unfeasible. The output code would be enormous and very complex. For these reasons, it was decided not to fully translate Fortran I/O statements, but rather to provide a library of routines available at runtime to make I/O possible.

Interface to Macsyma

Much of the code for the translator has been written and is currently being tested and debugged. When this is completed, the next phase of the project will involve interfacing the IMSL library routines with Macsyma.

The primary means of passing information to and retrieving information from a Fortran subroutine is by side-effecting upon its arguments. Because of this, functions will be needed to convert Macsyma objects into an appropriate data format for the translated Fortran subroutines to handle, and to pick up appropriate values from the Fortran workarea and restore them to the Macsyma caller. How much of the coding for this interface can be done by some automatic process is an unresolved question; not enough work has been done yet in this area to provide an adequate answer. Hand-written interfaces to code output by the translator have produced some examples of the syntax that a Macsyma user might expect to be confronted with when translated library routines become available. (See Figure 6.)

(C1) /* LEQT1F Solves a set of linear equations AX=B for X given an N by N matrix A. Its args are: N - Order of A and number of rows in B. M - Number of right-hand sides. IA - Number of rows in the dimension statement for A and B in the calling program. IDGT - Input option. IF positive, number of digits of accuracy in input. If 0, ignore accuracy tests in routine. A - A matrix of coefficients (N x N). B - An N x M matrix containing the right hand sides of the AX=B problem. Works via Gaussian elimination (Crout algorithm) with equilibration and partial pivoting. */ LEQT1F(3, 4, 4, 3, /* Args N, M, IA, IDGT */ MATRIX([ 33.0, 16.0, 72.0, 0.0], /* Matrix A */ [ -24.0, -10.0, -57.0, 0.0], [ -8.0, -4.0, -17.0, 0.0], [ 0.0, 0.0, 0.0, 0.0]), MATRIX([ 1.0, 0.0, 0.0, -359.0], /* Matrix B */ [ 0.0, 1.0, 0.0, 281.0], [ 0.0, 0.0, 1.0, 85.0], [ 0.0, 0.0, 0.0, 0.0])); Time= 82 msec. [ - 8.0 - 4.0 - 17.0 0.0 ] [ - 9.6666666 - 2.66666666 - 32.0 1.0 ] [ ] [ ] [ 3.0 2.0 - 6.0 0.0 ] [ 8.0 2.5 25.5 - 2.0 ] (D1) [[ ], [ ], 0] [ - 4.125 - 0.25 0.375 0.0 ] [ 2.66666666 0.666666664 9.0 - 5.0 ] [ ] [ ] [ 0.0 0.0 0.0 0.0 ] [ 0.0 0.0 0.0 0.0 ] /* Note that the return values are (respectively) (1) A lower triangular matrix L where A=L*LTRANSPOSE. L is stored in symmetric storage mode with the diagonal elements of L in reciprocal form. (2) The N x M solution matrix X (3) Error parameter */

Figure 6. How Macsyma will interface to IMSL

References

Acknowledgements

I wish to acknowledge my sincere thanks to the many people who have helped in this effort. Especially to Michael R. Genesereth and Richard L. Bryan for writing an early version of the Fortran parser and for a great deal of good advice about design decisions; Joel Moses for advice, concern and prodding; and John M. Favaro, for helping to debug the parser by using it.

Original printed text document

Copyright 1979, Kent M. Pitman. All Rights Reserved. HTML hypertext version of document

Copyright 1998, Kent M. Pitman. All rights reserved.

The following limited, non-exclusive, revokable licenses are granted: Browsing of this document (that is, transmission and display of a temporary copy of this document for the ordinary purpose of direct viewing by a human being in the usual manner that hypertext browsers permit such viewing) is expressly permitted, provided that no recopying, redistribution, redisplay, or retransmission is made of any such copy. Bookmarking of this document (that is, recording only the document's title and Uniform Resource Locator, or URL, but not its content, for the purpose of remembering an association between the document's title and the URL, and/or for the purpose of making a subsequent request for a fresh copy of the content named by that URL) is also expressly permitted. All other uses require negotiated permission.

Click here for an index of other titles by Kent Pitman.