{
                      FUNCTIONS AND PROCEDURES
                     FOR CREATING AND ACCESSING
                      LINKED LISTS IN HEAPSPACE

                     GARRY J. VASS  [72307,3311]


This file is a stand-alone TPascal program that attempts to provide
a step-by-step demo of the creation and management of a linked list
in heapspace.  Comments, revisions, corrections, questions, and
suggestions should be passed on to my SIG at the POLICE STATION BSS
(201-866-7902).



INTRODUCTION
------------

  Why bother with linked lists?  Why not just stick everything in an
   array and keep track it that way?  Link lists are undoubtedly one
   of the most arcane things Turbo has to offer; consider the problems
   in learning linked lists:

               1.  The variables do not really have names in any
                   familar context;
               2.  Lists involve odd notation;
               3.  Lists reside in an unfamilar place, heapspace.
               4.  Lists need pointer variables, and those look
                   cumbersome.
               5.  Lists require odd procedures like GETMEM, NEW,
                   and MEMAVAIL.
               6.  The TURBO documentation on this subject seems
                   vague and inconsistent.  It does not provide
                   an adequate conceptual foundation.

    On the other hand, consider some of the advantages of lists:

               1.  The total data area available to your application
                   is limited only by the amount of RAM available
                   when your program starts.  With arrays, your
                   application is limited by the size of the
                   data segment.
               2.  The sort time is reduced to that required for a
                   single pass of the data (no nested FOR constructs,
                   or bubble sorts, etc).
               3.  List members can be deleted much more easily than
                   array elements.  Imagine trying to delete the
                   70th element of an array with 300 plus elements.
               4.  The device drivers, memory control blocks, etc.,
                   used by DOS are chained together using linked list-like
                   constructs and are easier to understand and process
                   in a linked list context.


    So, on balance, lists have a useful place in any programmer's
    bag of trick,s and, with an equitable foundation, they are actually
    quite simple.

    A pointer, for example, is roughly analogous to an array index.  It
    has a value in its own right, but actually refers to an address
    (or offset) in memory of what is really needed.  An array index,
    however, contains only a part of what is needed to access the
    data (we rely upon the compiler to fill in the segment and base
    address of the array).  A pointer variable contains a full
    absolute address (segment and offset) in two words.  A pointer
    is declared in a way that seems to violate Turbo's syntax rules in
    that a reference is made to something not yet declared.  For example,

                      TYPE
                          LIST_POINTER = ^LISTREC;
                          (... more type declarations ...)
                      VAR
                          MYPOINTER : LIST_POINTER;

     When the compiler reaches this statement, it trusts that something
     called LISTREC will be defined in the next statement.  The english
     translation for this example is "Variables of type LIST_POINTER
     will contain two word, segment/offset addresses corresponding
     to the locations of variables of the type LISTREC.  Variables
     of the type LISTREC will not reside in the data segment like
     static variables.  Rather, they will reside in heapspace.  MYPOINTER
     is a variable that can contain any segment/offset address, but will
     contain the address of a specific member of a list (of type LISTREC)."

    To get the next element in an array, we usually just add one to the
    index.  To get the next element in a linked list, we need to know
    its address.  To accomplish this then, each member of a linked list
    must have some address information, namely the address of the
    next member.  For this reason, a RECORD structure is frequently used
    to define list members, such as:

                 TYPE
                     ANYSTRING    =  STRING[255];
                     LIST_POINTER = ^LISTREC;
                     LISTREC = RECORD
                                     POINTER_TO_NEXT_MEMBER: LIST_POINTER;
                                     MEMBER_DATA           : ANYSTRING;
                               END;
                 VAR
                    MYPOINTER : LIST_POINTER;
                    ROOT      : LIST_POINTER;
                    LIST      : LISTREC;

    This translates into, "each member of my linked list will occupy
    260 bytes of memory:

              POINTER_TO_NEXT_MEMBER      4 bytes
              MEMBER_DATA
                  (Turbo's length byte)   1 byte
                  (String part)         255 bytes

    Further, each member will reside in contiguous heapspace, and each
    member will contain the address of the next member.  The variable,
    ROOT, will be used to hold the address of the first member added to
    the list ('list entry point').  With the value of ROOT, the first
    list member can be accessed.  Once the first record is accessed, the
    pointer to the next record can be obtained.  When that record is
    obtained, the pointer to the third record can be found, and so on".

    ROOT, then, corresponds roughly to the first element of an array.
    To learn the last element in an array, we frequently use a counter
    in a context like "FOR I := 1 TO NAME_COUNT DO ...".  In linked
    lists, the last member is signified by all zeros in the
    POINTER_TO_NEXT_MEMBER element.  Turbo uses a pre-defined constant
    called, "NIL" for this.  Using the above example, compare these
    constructs:

      last element of array               last member of a list

      I := I + 1;                         MYPOINTER := MYPOINTER^.POINTER_TO_NEXT_MEMBER;
      IF I = NAME_COUNT THEN              IF MYPOINTER = NIL THEN
         BEGIN                               BEGIN
             (last element is reached)           (last member is reached)

    The translation for the statements on the right is "MYPOINTER is currently
    pointing to a list member.  Take the value of the POINTER_TO_NEXT_MEMBER element
    from this record and put it in MYPOINTER.  If this is NIL, then..."

    With these concepts, we can now transverse a linked list in a line-by-line
    analogy to array processing:

        array logic                            linked list logic

        I := 1;                                MYPOINTER := ROOT;
        REPEAT                                 REPEAT
            WRITELN(NAMES[I]);                      WRITELN(MYPOINTER^.MEMBER_DATA);
            I := SUCC(I);                           MYPOINTER := MYPOINTER^.POINTER TO NEXT MEMBER;
        UNTIL I > NAME_COUNT;                  UNTIL MYPOINTER = NIL;

    To add an element/member:

        array logic                   linked list logic

  NAME_COUNT := SUCC(NAME_COUNT);     NEW(MYPOINTER);
  NAMES[NAME_COUNT] := 'Mr. Kahn';    MYPOINTER^.MEMBER_DATA := 'Mr. Kahn';
                                      MYPOINTER^.POINTER_TO_NEXT_MEMBER := NIL;
                                      (logic for setting pointer
                                       in previous record goes here).

  Beyond these examples, the analogies tend to break down.  The logic for
  sorting an array tends to involve a physical rearrangement of the data
  through an iterative process.  A linked list is usually sorted as
  it is created by reassigning the pointers.  The upper bound of an
  array is bound to a constant.  The capacity of a list is tied to
  available heapspace.

  To maximize the value of this program, I suggest that it be run
  in its native form (i.e., as you found it - which is hopefully
  not altered) several times and observe the dynamics/interaction
  of the generic routines.  Then copy this program into your own
  file and modify the list building routines to create various
  situations, such as a stack/heap collisions, duplicate records,
  multiple lists, different sort key types, and so on.  It is also
  important to note that several particularly elegant uses of
  pointer variables that are not list related can be found on "GO BOR"
  (try MAPMEM.PAS by TurboPower for a flagship example).

TREATMENT
---------

   The linked list used here is composed of three elements.  The first
   is an index (or sort key).  The values in this element determine
   how the list is to be sorted.  I have used an integer type here,
   but this element can just as easily be redefined as a string type.
   In addition to being the sort key for the list, the index element
   is also used as a search argument in the lookup function.  The
   next element is the so-called "data part", which contains all the
   relevant information associated with the index value.  Naturally, this
   element can also be redefined to meet specific applications.  The last
   element is a pointer variable that can hold one of two values:

           1.  the NIL value, a special value indicating that
               this record is the last item in the list.  NIL
               is represented internally by four zero filled
               bytes; or
           2.  a pointer to the location of the next item in the
               list.  These four bytes are represented internally
               (as are all pointers) as:

                      -  low order offset;
                      -  high order offset;
                      -  low order segment;
                      -  high order segment;

               It is IMPORTANT to note that the physical location
               of a pointer or list member has NOTHING to do with
               its sorted (or logical) location in the list.

               Physical addresses are assigned at the next available
               heapspace location in the order that you ask for them.
               This means that the root record (or logical entry point)
               of a list could be located at a higher physical address than
               any of the other members in the same list.

   Most of the procedures and functions given below deal with the
   assignment of these pointers.  Filling the index and data parts
   is up to you.  To build a list of names and ages
   with age as the index, for example, the following code could be
   used:
              list.index_part := 28;   (* age *)
              list.data_part  := 'Sally Maxwell';
              add_member(list);
              list.index_part := 27;   (* age *)
              list.data_part  := 'Jane Robinson';
              add_member(list);
              etc, etc, etc....

   The add_member procedure finds the appropriate place in the
   list, plugs your record there, and figures out to modify the
   pointers.

   Once the list has been constucted, several things can be
   done:

         1.  Look up a member of the list based upon the
             the index value (procedure lookup_member).
         2.  Delete a member of the list based upon
             the index value (procedure delete_member).
         3.  Process the entire list in sorted order
             (procedure process_list).
         4.  Dispose of the entire list (procedure dispose_list).

  When reading through linked list code, one encounters odd notation
  such as the carat, ^.  I use the following translations:


      P^              That which is pointed to by P.
      P^ := X         That which is pointed to by P is X.
      P^.A            The A element of the record pointed
                      to by P.
      P^ := P^.M^     P now points to same thing as the M pointer
                      element of the record being pointed to by P.
      NEW(W)          Create a new record in heapspace.  Assignments
                      for the record elements to go in this area will
                      soon follow, and will be issued in the form...

                               W^.INDEX :=
                               W^.DATA  :=
}
PROGRAM LINKED_LISTS;
CONST
     FOREVER:BOOLEAN = FALSE;  { FOR CREATING STACK/HEAP COLLISIONS }
TYPE
    ANYSTRING = STRING[255];
    LISTPTR =  ^LISTREC;
    LISTREC =   RECORD
                      NEXT_POINTER : LISTPTR;
                      INDEX_PART   : INTEGER;
                      DATA_PART    : ANYSTRING;
                END;
VAR
{  Define a global variable to hold the root address,
   or entry point of the list.  The concept of a root
   variable means "this variable holds the logical
   entry point into the list".}
   ROOT: LISTPTR;

{  Define a global variable to enable the construction
   of the list.}
   LIST: LISTREC;

{  Extra pointer for general use }
   TEMPTR:LISTPTR;

   ANYINTEGER:INTEGER;
   ANYCH:CHAR;


{--------------------- GENERAL SUPPORT ROUTINES  ----------------------}
{ These routines are not of immediate interest and are included in compressed format.}
FUNCTION HEXBYTE(N:INTEGER):ANYSTRING;CONST H:ANYSTRING='0123456789ABCDEF';BEGIN HEXBYTE:=H[((LO(N)DIV 16)MOD 16)+1]+
H[(LO(N)MOD 16)+1];END;FUNCTION HEXWORD(N:INTEGER):ANYSTRING;BEGIN HEXWORD:=HEXBYTE(HI(N))+HEXBYTE(LO(N));END;
FUNCTION CONV(I:INTEGER):REAL;BEGIN CONV:=256.0*HI(I)+LO(I);END;PROCEDURE PAUSE;VAR CH:CHAR;BEGIN WRITELN;
WRITELN('-- PAUSING FOR KEYBOARD INPUT...');READ(KBD,CH);WRITELN;END;
{--------------------- END OF GENERAL SUPPORT ROUTINES  ----------------}


{  Linked lists reside in heapspace, an area of unused memory
   at the low end of the stack/data segment.  The beginning of
   available heapspace changes according to two factors:

              1.  how much is on the stack; and
              2.  how much heapspace has already been grabbed.

   Our friends at Borland were thoughtful enough to provide
   a pre-defined variable that points to the beginning of heapspace,
   this variable is called, "HEAPPTR", and is configured with four bytes
   just like the pointer layout described above.  You can use the
   VALUEOF(HeapPtr) function at various points in the program to
   examine Turbo's behavior in setting and assigning this rather
   ephermeral item.  Unfortunately the standard variable, StackPtr,
   as described in the documentation, is more elusive and not
   compatable with VALUEOF.

   When heapspace is requested (with the NEW procedure), the HEAPPTR
   variable will be reset to the next available location in
   memory so that it will always point to the beginning of
   available heapspace.  If enough heapspace is requested,
   this variable will ultimately be equal to, or greater than
   the stack pointer.  This situation creates a run-time
   "stack/heap collision" and the program dies.  To avoid
   this, the MEMAVAIL function should be used before the
   NEW procedure to assure that enough heapspace is available.
}

{-------------------  LINKED LIST SUPPORT  ---------------------

These routines help show what is happening as the list is being
constructed.  They are not needed for an application.
                                                                }
PROCEDURE SNAPMEM(X, Y:INTEGER);     { PUTS AVAILABLE HEAPSPACE }
BEGIN                                { AT COLUMN X, ROW Y       }
     GOTOXY(X, Y);
     CLREOL;
     WRITELN('HEAP SPACE = ',CONV(MEMAVAIL)*16.0:8:0);
END;

FUNCTION VALUEOF(P:LISTPTR):ANYSTRING;  { RETURNS A STRING IN   }
                                        { SEGMENT:OFFSET FORMAT }
                                        { WHICH CONTAINS VALUE  }
BEGIN                                   { OF A POINTER VARIABLE }
     VALUEOF := HEXBYTE(MEM[SEG(P):OFS(P) + 3]) +
                HEXBYTE(MEM[SEG(P):OFS(P) + 2]) +':'+
                HEXBYTE(MEM[SEG(P):OFS(P) + 1]) +
                HEXBYTE(MEM[SEG(P):OFS(P) + 0]);
END;

PROCEDURE GIVESTATS;   { REPORTS STATISTICS FOR DEMO PROGRAM }
BEGIN
     WRITELN('HEAP SPACE NOW BEGINS AT ',VALUEOF(HEAPPTR),
            ' AND EXTENDS FOR ',CONV(MEMAVAIL) * 16.0:8:0,' BYTES');
     PAUSE;
END;

PROCEDURE SNAP_MEMBER(POINTER:LISTPTR);   {  PRINTS THE LIST MEMBER }
BEGIN                                     {  BEING POINTED TO BY "POINTER"}
     IF POINTER = NIL THEN
        BEGIN
             WRITELN('-----------  EMPTY LIST  -------------');
             EXIT;
        END;
    WRITELN('---------------  LIST MEMBER INFORMATION  ------------');
    WRITELN('THE INDEX OF THIS RECORD IS ', POINTER^.INDEX_PART);
    WRITELN('THE DATA IN THIS RECORD IS  ', POINTER^.DATA_PART);
    WRITELN('MEMORY LOCATION OF THIS RECORD IS ',VALUEOF(POINTER));
    WRITELN('THIS RECORD IS POINTING TO A RECORD AT ',VALUEOF(POINTER^.NEXT_POINTER));
    PAUSE;
END;
{---------------------  END OF LINKED LIST SUPPORT  ---------------------}


{---------------------  GENERIC LINKED LIST ROUTINES  --------------------}

{  When adding a member to a linked list, four situations can occur.

   1.  The list is empty/uninitialized.
   2.  The new record must be added to a place
       before the existing root record.
   3.  The new record must be inserted somewhere
       in the middle of the list.
   4.  The new record must be inserted
       at the end of the list.

  The add_member procedure below recognizes each of these four
  situations and adjusts the list accordingly.

}

PROCEDURE ADD_MEMBER (TARGET:LISTREC);   { ADDS THE TARGET RECORD TO }
VAR                                      { LINKED LIST               }
     MOVING_POINTER          : LISTPTR;
     POINTER_TO_PRIOR_RECORD : LISTPTR;
BEGIN

     {Assure that sufficient memory is available for the target member}
     IF ((CONV(MEMAVAIL) * 16.0) < CONV(SIZEOF(TARGET))) THEN
        BEGIN
             WRITELN('---------- NO MORE MEMORY ---------------');
             EXIT;
        END;

     WRITELN;
     WRITELN('-------------  ADDING A RECORD TO THE LIST  ----------');
     WRITELN;
     WRITELN('INDEX VALUE IS ',TARGET.INDEX_PART,' DATA IS ',TARGET.DATA_PART);


{ Logic for adding the first record to a list }
     IF ROOT = NIL THEN
        BEGIN
             { Allocate some memory for the root pointer }
             NEW(ROOT);
             { Assign NIL to the last record in the list }
             TARGET.NEXT_POINTER := NIL;
             { Root points to the record in TARGET }
             ROOT^ := TARGET;
             WRITELN('THIS RECORD INITIALIZED THE LIST AT ',VALUEOF(ROOT));
             GIVESTATS;
             EXIT;
        END;


{ Logic for adding a record "in front of" the current root }
     IF (TARGET.INDEX_PART) <= (ROOT^.INDEX_PART) THEN
        BEGIN
             { Set pointer in new record to point to the root }
             TARGET.NEXT_POINTER := ROOT;
             { Allocate some memory for a new root pointer }
             NEW(ROOT);
             { Root points to the record in TARGET }
             ROOT^ := TARGET;
             WRITELN('THIS RECORD BECAME THE NEW ROOT AT    ',VALUEOF(ROOT));
             WRITELN('THIS RECORD POINTS TO THE OLD ROOT AT ',VALUEOF(ROOT^.NEXT_POINTER));
             GIVESTATS;
             EXIT;
        END;

{ Logic for adding a record to "the middle of" a list }
     MOVING_POINTER := ROOT;
     REPEAT
           POINTER_TO_PRIOR_RECORD  := MOVING_POINTER;
           MOVING_POINTER  := MOVING_POINTER^.NEXT_POINTER;
           IF (MOVING_POINTER <> NIL) THEN
               IF ((TARGET.INDEX_PART) <= (MOVING_POINTER^.INDEX_PART))
                   THEN
                        BEGIN
                             NEW(POINTER_TO_PRIOR_RECORD^.NEXT_POINTER);
                             TARGET.NEXT_POINTER := MOVING_POINTER;
                             POINTER_TO_PRIOR_RECORD^.NEXT_POINTER^ := TARGET;
                             WRITELN('THIS RECORD WAS INSERTED INTO THE MIDDLE OF THE LIST ',
                                     'AT ',VALUEOF(POINTER_TO_PRIOR_RECORD^.NEXT_POINTER));
                             GIVESTATS;
                             EXIT;
                        END;
     UNTIL MOVING_POINTER = NIL;

     {APPEND TO END OF LIST }
     NEW(MOVING_POINTER);
     TARGET.NEXT_POINTER := NIL;
     POINTER_TO_PRIOR_RECORD^.NEXT_POINTER := MOVING_POINTER;
     MOVING_POINTER^ := TARGET;
     WRITELN('THIS RECORD WAS ADDED TO THE END OF THE LIST AT ',VALUEOF(POINTER_TO_PRIOR_RECORD^.NEXT_POINTER));
     GIVESTATS;
END;

FUNCTION LAST_MEMBER:LISTPTR;   { RETURNS POINTER TO LAST LIST MEMBER }
VAR P:LISTPTR;
BEGIN
     P := ROOT;
     IF P = NIL THEN
        BEGIN
             LAST_MEMBER := NIL;
             EXIT;
        END;
     REPEAT
           IF P^.NEXT_POINTER <> NIL THEN P := P^.NEXT_POINTER;
     UNTIL P^.NEXT_POINTER = NIL;
     LAST_MEMBER := P;
END;

PROCEDURE ROTATE_LIST_LEFT;      { ROTATES ENTIRE LIST LEFT, ROOT BECOMES }
VAR P1:LISTPTR;                  { TAIL, TAIL BECOMES PENULTIMATE, ETC.   }
    P2:LISTPTR;
    TR:LISTPTR;
    R1:LISTREC;
BEGIN
     IF (ROOT = NIL) OR (ROOT^.NEXT_POINTER = NIL) THEN EXIT;
     R1.DATA_PART    := ROOT^.DATA_PART;
     R1.INDEX_PART   := ROOT^.INDEX_PART;
     R1.NEXT_POINTER := NIL;
     TR := ROOT^.NEXT_POINTER;
     DISPOSE(ROOT);
     ROOT := TR;
     NEW (P1);
     P1^ := R1;
     P2 := LAST_MEMBER;
     P2^.NEXT_POINTER := P1;
END;

PROCEDURE MAKE_NEW_HEAD(P:LISTPTR);  { FIXES LIST SO THAT RECORD WITH POINTER }
BEGIN                                { "P" IS THE ROOT RECORD.  ASSUMES "P"   }
     IF (ROOT = NIL) OR              { EXISTS.                                }
        (ROOT^.NEXT_POINTER = NIL) THEN EXIT;
     REPEAT
           ROTATE_LIST_LEFT;
     UNTIL ROOT = P;
END;

FUNCTION LIST_COUNT:INTEGER;     { RETURNS LIST MEMBER COUNT }
VAR
   P:LISTPTR;
   I:INTEGER;
BEGIN
     IF ROOT = NIL THEN
        BEGIN
             LIST_COUNT := 0;
             EXIT;
        END;
     I := 0;
     P := ROOT;
     REPEAT
           I := SUCC(I);
           P := P^.NEXT_POINTER;
     UNTIL P = NIL;
     LIST_COUNT := I;
END;

FUNCTION PENULTIMATE:LISTPTR;           { RETURNS PENULTIMATE LIST MEMBER  }
VAR                                     { OR NIL IF NO PENULTIMATE         }
    P1:LISTPTR;
    P2:LISTPTR;
BEGIN
     IF ROOT = NIL THEN
        BEGIN
             PENULTIMATE := NIL;
             EXIT;
        END;
     IF LIST_COUNT <= 2 THEN
        BEGIN
             PENULTIMATE := ROOT;
             EXIT;
        END;
     P1 := ROOT;
     P2 := ROOT^.NEXT_POINTER;
     REPEAT
           P1 := P2;
           P2 := P2^.NEXT_POINTER;
     UNTIL P2 = LAST_MEMBER;
     PENULTIMATE := P1;
END;

PROCEDURE KILL_MEMBER(P:LISTPTR);   { DELETES LIST MEMBER POINTED TO      }
VAR P1:LISTPTR;                     { BY "P".  EXITS IF THE LIST CONTAINS }
    P2:LISTPTR;                     { LESS THAN TWO MEMBERS.              }
BEGIN
     IF ROOT = NIL THEN EXIT;
     IF ROOT^.NEXT_POINTER = NIL THEN EXIT;
     IF P = ROOT THEN
        BEGIN
             P1 := ROOT^.NEXT_POINTER;
             DISPOSE(P);
             ROOT := P1;
             EXIT;
        END;
     IF P = LAST_MEMBER THEN
        BEGIN
             P1 := PENULTIMATE;
             DISPOSE(P);
             P1^.NEXT_POINTER := NIL;
             EXIT;
       END;
     P1 := ROOT;
     REPEAT
           P2 := P1;
           IF P1^.NEXT_POINTER <> NIL THEN P1 := P1^.NEXT_POINTER;
     UNTIL (P1 = NIL) OR
           (P1 = P);
     IF P1 = NIL THEN EXIT;
     P2^.NEXT_POINTER := P1^.NEXT_POINTER;
     DISPOSE(P1);
END;

FUNCTION LOOKUP_MEMBER(IND:INTEGER):LISTPTR;  { RETURNS THE POINTER      }
VAR                                           { CONTAINING THE RECORD    }
   MOVING_POINTER:LISTPTR;                    { WHOSE INDEX VALUE IS     }
BEGIN                                         { EQUAL TO "IND".  IF NO   }
     MOVING_POINTER := ROOT;                  { RECORD EXISTS, RETURNS   }
     REPEAT                                   { NIL.                     }
           IF MOVING_POINTER^.INDEX_PART = IND THEN
              BEGIN
                   LOOKUP_MEMBER := MOVING_POINTER;
                   EXIT;
              END;
           MOVING_POINTER := MOVING_POINTER^.NEXT_POINTER;
    UNTIL MOVING_POINTER = NIL;
    LOOKUP_MEMBER := NIL;
END;

{------------------  END OF GENERIC LIST PROCEDURES  -------------}


{---------------  PRESENTATION PROCEDURES AND ENGINE  ----------}

PROCEDURE PROCESS_LIST;                  { WALKS THROUGH THE LIST, }
                                         { FROM ROOT TO NIL.       }

{ IMPORTANT NOTE ABOUT LIST PROCESSING!!!!

       Once a list has been created you can still have a stack/heap
       collision where the stack is the culprit!  Remember that each
       call to a function or procedure moves the stack downward
       closer to your list.  For this reason, extra care must be
       taken to assure that the routines for list processing programs
       are sufficiently "shallow" (not nested too deep, not overly
       recursive, etc). }
VAR
   MOVING_POINTER:LISTPTR;
BEGIN
     CLRSCR;
     {                                       }
     { YOUR LIST PROCESSING ROUTINES GO HERE }
     {                                       }
     IF ROOT = NIL THEN
        BEGIN
             WRITELN('--------------  EMPTY LIST  -------------');
             EXIT;
        END;
     WRITELN;
     WRITELN('      ----------------  CURRENT LIST CONTENTS  -------------- ');
     WRITELN;
     WRITELN('INDEX         DATA               LOCATION OF RECORD           LOCATION OF NEXT');
     WRITELN('VALUE        VALUE                  IN MEMORY                RECORD IN MEMORY');
     WRITELN('-----  --------------------         ---------                ----------------');
     MOVING_POINTER := ROOT;
     REPEAT
          {  LIST PROCESSING CODE GOES HERE }
          WRITELN(MOVING_POINTER^.INDEX_PART:5,
                  MOVING_POINTER^.DATA_PART :20,
                  VALUEOF(MOVING_POINTER):20,
                  VALUEOF(MOVING_POINTER^.NEXT_POINTER):30);
          MOVING_POINTER := MOVING_POINTER^.NEXT_POINTER;
     UNTIL MOVING_POINTER = NIL;
     WRITELN;
     WRITELN('THE LIST COUNT IS ',LIST_COUNT);
     IF ROOT = NIL THEN WRITELN('----- EMPTY LIST ----')
                   ELSE WRITELN('THE ROOT MEMBER IS ',ROOT^.DATA_PART);
     MOVING_POINTER := PENULTIMATE;
     IF MOVING_POINTER <> NIL
        THEN WRITELN('THE PENULTIMATE MEMBER IS ',MOVING_POINTER^.DATA_PART)
        ELSE WRITELN('THERE IS NO PENULTIMATE MEMBER');
     MOVING_POINTER := LAST_MEMBER;
     WRITELN('THE LAST MEMBER IS ',MOVING_POINTER^.DATA_PART);
     PAUSE;
END;

PROCEDURE BUILD_A_LIST;
BEGIN
     {                                     }
     { YOUR LIST BUILDING LOGIC GOES HERE  }
     {                                     }
     LIST.INDEX_PART :=  25; LIST.DATA_PART  := 'George Wilson  '; ADD_MEMBER(LIST); PROCESS_LIST;
     LIST.INDEX_PART :=  21; LIST.DATA_PART  := 'Walter Thompson'; ADD_MEMBER(LIST); PROCESS_LIST;
     LIST.INDEX_PART :=  28; LIST.DATA_PART  := 'James McCoy    '; ADD_MEMBER(LIST); PROCESS_LIST;
     LIST.INDEX_PART :=  22; LIST.DATA_PART  := 'Robert Henton  '; ADD_MEMBER(LIST); PROCESS_LIST;
     LIST.INDEX_PART :=  31; LIST.DATA_PART  := 'Dennis Campbell'; ADD_MEMBER(LIST); PROCESS_LIST;
     LIST.INDEX_PART :=  20; LIST.DATA_PART  := 'Jack Farrell   '; ADD_MEMBER(LIST); PROCESS_LIST;
     LIST.INDEX_PART :=  24; LIST.DATA_PART  := 'Herbert Walker '; ADD_MEMBER(LIST); PROCESS_LIST;
END;

{-----------------  BEGINNING OF MAINLINE  ------------------------}
{------------------------  DEMO  ----------------------------------}
BEGIN
     CLRSCR;
     WRITELN('PROGRAM IS LOADED AT ',HEXWORD(CSEG));
     WRITELN('DATA SEGMENT IS AT ',HEXWORD(DSEG));
     WRITELN('STACK SEGMENT IS AT ',HEXWORD(SSEG));
     WRITELN('HEAP SPACE BEGINS AT ',VALUEOF(HEAPPTR),
             ' AND EXTENDS FOR ',    CONV(MEMAVAIL) * 16.0:8:0,
             ' BYTES');
     WRITELN('LINKED LIST RECORD SIZE IS ',SIZEOF(LIST),' BYTES');
     PAUSE;
     ROOT := NIL;

     WRITELN('BEGIN BUILDING A LINKED LIST');
     PAUSE;
     BUILD_A_LIST;

     REPEAT
           WRITELN('DO YOU WANT TO ROTATE THE LIST (Y/N)?');
           GOTOXY(50, WHEREY - 1);
           READ(KBD,ANYCH);
           IF UPCASE(ANYCH) = 'Y' THEN
              BEGIN
                   ROTATE_LIST_LEFT;
                   PROCESS_LIST;
              END;
     UNTIL UPCASE(ANYCH) <> 'Y';
     WRITELN;
     WRITELN('NOW SEARCH FOR LIST MEMBERS');
     PAUSE;
     REPEAT
          PROCESS_LIST;
          WRITELN('PLEASE ENTER AN AGE TO SEARCH FOR (OR ZERO TO QUIT):  ');
          GOTOXY(55, WHEREY - 1);
          READLN(ANYINTEGER);
          IF ANYINTEGER <> 0 THEN
                 BEGIN
                      WRITELN;
                      TEMPTR := LOOKUP_MEMBER(ANYINTEGER);
                      IF TEMPTR = NIL THEN
                                          BEGIN
                                               WRITELN('NO ONE OF AGE ',ANYINTEGER);
                                               PAUSE;
                                          END
                                      ELSE SNAP_MEMBER(TEMPTR);
                 END;
     UNTIL ANYINTEGER = 0;

     WRITELN('NOW DELETE LIST MEMBERS');
     PAUSE;
     REPEAT
          PROCESS_LIST;
          WRITELN('PLEASE ENTER AN AGE TO DELETE (OR ZERO TO QUIT):  ');
          GOTOXY(55, WHEREY - 1);
          READLN(ANYINTEGER);
          IF ANYINTEGER <> 0 THEN
                 BEGIN
                      WRITELN;
                      TEMPTR := LOOKUP_MEMBER(ANYINTEGER);
                      IF TEMPTR = NIL THEN
                                          BEGIN
                                               WRITELN('NO ONE OF AGE ',ANYINTEGER);
                                               PAUSE;
                                          END
                                      ELSE KILL_MEMBER(LOOKUP_MEMBER(ANYINTEGER));
                 END;
     UNTIL (ANYINTEGER = 0) OR (ROOT = NIL);
     PROCESS_LIST;
END.

{-----------------------  END OF MAINLINE  ---------------------------}

APPLICATIONS:

With no effort at all, you can use these procedures to develop an improved
DOS sort filter.  Consider the following list record:

      LISTREC = RECORD
                      INDEX_PART : ANYSTRING;  { Load this according  }
                                               { to your specs        }

                      DATA_PART  : ANYSTRING;  { Line from ASCII file }
                      NEXT_PART  : POINTER;
                END;

With the numerous directory procedures available at "GO BOR", you can
develop customized directory sort programs.  A sort by file size, for
example, might have the following records:

         DIRREC  = RECORD
                         ATTRIBUTE     : BYTE;
                         NAME          : ANYSTRING;
                         EXTENSION     : ANYSTRING;
                         DATE          : DATEREC;  { or whatever you want }
                         TIME          : TIMEREC;  { "      "     "   "   }
                         FST_CLUSTER   : INTEGER;
                   END;

         LISTREC = RECORD
                         INDEX_PART : REAL;    { Holds file size }
                         DATA_PART  : DIRREC;
                         NEXT_PART  : POINTER;
                   END;
