{$U-}
{$C-}
{
 TYPEX.PAS  Jim Mischel, June 1, 1986

 Program listing and variable cross-reference generator for
 Turbo Pascal programs.

 Usage is TYPEX <source> [<destination>] [;<options>]
    Options are: I - INCLUDE files also
                 X - Create program Cross-reference
    Defaults:
       Output   - LST:
       Includes - NO
       Xref     - NO

 If memory size is a consideration, INITIALIZE, PROCESS_FILE, and PRINT_XREF
 can be made overlay procedures, with a savings of approximately 2.5K bytes.

 This program evolved from LISTER.PAS that was included on the Turbo Pascal
 distribution disk.  Some of the original code still exists.

 The procedure GETDATE may have to be changed for use with MS-DOS.
 It will NOT work with CP/M 2.2 without modification.  It will work
 with MP/M, CP/M 3.x, and TurboDOS 1.3 or higher.

 This program was written using Turbo Pascal version 3.0 for CP/M.  I have
 not tested it on any other operating system, though it should work except
 as noted above.

 MODIFICATIONS:

 06/01/86 - jim - Initial coding.

 10/21/86 - jim - Use a pointer-reversal in PRINT_REFS in place of the
                  recurrsive list traversal.

 11/30/86 - jim - Make the tree a right in-threaded tree.  This speeds
                  printing of the cross-reference.
                  Add the FSTPTR field to the node record.  References are
                  now added in order of occurance.  FSTPTR points to the
                  first reference record, and NXTPTR points to the last.
                  Also added NUMREFS to the record to prevent having
                  to scan the list twice.  PRINT_REFS is now a simple linked
                  list traversal procedure.
}
program typex;
const
 version_no    = '2.5';
 printwidth    = 70;                   { print width for each line }
 printlength   = 55;                   { # of lines to print on each page }
 pathlength    = 14;                   { maximum length of file name }
 default_output = 'LST:';              { default destination }
 include_default = false;              { default to no include files }
 xref_default  = false;                { default to no cross-reference }
 refs_per_line = 10;                   { max. number of references per line }
 max_id_len    = 15;                   { max. id length for references on same line }
 optchr        = ';';                  { option seperator character }

type
 filename      = string[pathlength];
 string8       = string[8];
 string255     = string[255];
 strptr        = ^string255;
 refptr        = ^reference;
 reference     = record                { item reference record }
                   line,               { source line of reference }
                   incl     : integer; { line in include file (if any) }
                   nxtptr   : refptr;  { pointer to next reference }
                 end;

 itmptr        = ^item;
 item          = record
                   idname : strptr;    { pointer to id name }
                   left,               { left node of binary tree }
                   right  : itmptr;    { right node of binary tree }
                   rthrd  : boolean;   { TRUE if right is thread pointer }
                   fstptr,             { pointer to first reference }
                   nxtptr : refptr;    { pointer to last reference }
                   numrefs : integer;  { Reference counter.  This is NOT a
                                         count of references to this ID.  It
                                         is used by PRINT_REFS to figure out
                                         how many lines it will take to print
                                         all the references for this item. }
                 end;
var
 page_no,                              { current page number }
 currow        : integer;              { current row in output file }
 outfile,                              { listing file }
 mainfile      : text;                 { source file }
 mainfilename  : filename;             { input file name }
 search        : array[1..4] of string[4]; { search strings for includes }
 date,                                 { date returned from get_date }
 time          : string8;              { time returned from get_date }
 dots          : string[70];           { line of dots for page header }
 xref,                                 { TRUE = generate cross-reference }
 includes      : boolean;              { TRUE = process include files }
 xref_head     : itmptr;               { root of cross-reference tree }

{ PAGE - move output to new page }
procedure page(var outfile : text);
const
 ff            = ^L;
begin
 write(outfile,ff);
end;

{ HEADINGS - move to new page and print headings. }
procedure headings;
begin
 page(outfile);
 page_no := page_no + 1;
 write(outfile,date:8);
 write(outfile,mainfilename:39);
 writeln(outfile,time:33);
 writeln(outfile,dots,'Page ',page_no:5);
 writeln(outfile);
 currow := 0;
end; { headings }

{ OPEN - open file FP with name NAME. Return TRUE if operation successful. }
function open(var fp : text; name : filename) : boolean;
begin
 assign(fp,name);
 {$i- turn off I/O error checking}
 reset(fp);
 {$i+ error checking back on}
 if ioresult <> 0 then
   begin
     open := false;
     close(fp);
   end
 else
   open := true;
end { open };

{ INITIALIZE - set parameters and open files }
procedure initialize;

{ GET_DATE - get date and time from system and convert to two strings.
            Date is stored as MM/DD/YY.  Time is stored as HH:MM:SS,
            with seconds set to 00.
            This routine will not work for dates prior to 01/01/78
}

procedure get_date(var date_ptr,time_ptr);
type
 month_array   = array[1..2,1..12] of integer;
 string8       = string[8];
var
 date          : string8 absolute date_ptr;
 time          : string8 absolute time_ptr;
 date_time     : packed array [1..4] of char;
 jdate         : integer absolute date_time; { #days since 12/31/77 }
 x,
 month         : byte;
 year          : integer;
const
 day_table     : month_array =
                 ((31,59,90,120,151,181,212,243,273,304,334,365),
                  (31,60,91,121,152,182,213,244,274,305,335,366));

{ LEAP - return TRUE if YEAR is a leap year }
function leap(year : integer) : boolean;
begin
 leap := (year mod 4 = 0) and (year <> 100);
end; {leap}

{ DAYS_IN - return number of days in YEAR }
function days_in(year : integer) : integer;
begin
 if (leap(year)) then days_in := 366
 else days_in := 365;
end; {days_in}

begin
 bdos(105,addr(date_time));            { get system date/time }
 time := '00:00:00';                   { initialize time }
 time[1] := chr((ord(date_time[3]) div 16) + 48); { hours first digit }
 time[2] := chr((ord(date_time[3]) mod 16) + 48); {       second digit }
 time[4] := chr((ord(date_time[4]) div 16) + 48); { minutes first digit }
 time[5] := chr((ord(date_time[4]) mod 16) + 48); {         second digit }

 year := 78;
 while (jdate > days_in(year)) do
   begin
     jdate := jdate-days_in(year);
     year := year + 1;
   end;

 if (leap(year)) then x := 2           { set proper date table }
 else x := 1;

 month := 1;
 while (jdate > day_table[x,month]) do { move us to the proper month }
   month := month + 1;
 if (month > 1) then
   jdate := jdate - day_table[x,month-1]; { and set the date }

 date := '00/00/00';
 date[1] := chr(month div 10 + 48);    { month first digit }
 date[2] := chr(month mod 10 + 48);    {       second digit }
 date[4] := chr(jdate div 10 + 48);    { day first digit }
 date[5] := chr(jdate mod 10 + 48);    { day second digit }
 date[7] := chr(year div 10 + 48);     { year first digit }
 date[8] := chr(year mod 10 + 48);     {      second digit }
end; { get_date }

{ PRINTUSE - print usage information and exit }
procedure printuse;
begin
 writeln;
 writeln('Turbo Pascal program listing and variable Cross-reference generator');
 writeln;
 writeln('Usage is TYPEX <source> [<destination>] [',optchr:1,'<options>]');
 writeln('   Options are: I - INCLUDE files also');
 writeln('                X - Create program Cross-reference');
 write  ('   DEFAULTS:  Output   - ');
 writeln(default_output);
 write  ('              Includes - ');
 if include_default then
   writeln('YES')
 else
   writeln('NO');
 write  ('              Xref     - ');
 if xref_default then
   writeln('YES')
 else
   writeln('NO');
 halt;
end; { printuse }

{ OPENMAIN - Open main input and output files.  Set XREF and INCLUDE options. }
procedure openmain;
var
 tmpstr,
 option_string : string[32];
 param         : byte;
 outfilename   : filename;             { output file name }

function get_param(var param : byte) : string255;
var
 x             : byte;
begin
 if (length(tmpstr) > 0) then
   begin                               { there's an option string here }
     get_param := tmpstr;
     tmpstr := '';
   end
 else
 if (param > paramcount) then
   get_param := ''                     { no more parameters }
 else
   begin
     tmpstr := paramstr(param);        { get next parameter }
     param := param+1;                 { bump parameter count }
     x := pos(optchr,tmpstr);
     if (x > 1) then                   { see if it's an option string }
       begin
         get_param := copy(tmpstr,1,x-1);    { this is the returned parameter }
         tmpstr := copy(tmpstr,x,length(tmpstr)-x+1); { save this for next time }
       end
     else
       begin
         get_param := tmpstr;          { return this }
         tmpstr := '';                 { nothing saved }
       end;
   end;
end; { get_param }

begin { openmain }
 if (paramcount = 0) then
   printuse;
 includes := include_default;          { set default parameters }
 xref := xref_default;
 tmpstr := '';
 option_string := '';
 param := 1;
 mainfilename := get_param(param);     { get input file name }
 if not (open(mainfile,mainfilename)) then
   begin
     writeln('ERROR - cannot open input file ',mainfilename);
     halt;
   end;
 outfilename := get_param(param);      { get output file name and options }
 if (length(outfilename) > 0) then
   if (outfilename[1] = optchr) then
     begin
       option_string := outfilename;   { options }
       outfilename := default_output;  { but no defined file name }
     end
   else
     option_string := get_param(param) { get options (if any) }
 else
   begin
     option_string := '';              { no options }
     outfilename := default_output;    { no defined file name }
   end;
 assign(outfile,outfilename);
 {$I-}
 rewrite(outfile);
 {$I+}
 if (ioresult <> 0) then
   begin
     writeln('ERROR - cannot open output file ',outfilename);
     halt;
   end;
 if (pos(optchr,option_string) = 1) then
   begin                               { set options }
     includes := (include_default xor (pos('I',option_string) > 0));
     xref := (xref_default xor (pos('X',option_string) > 0));
   end;
end {openmain};

begin {initialize}
 openmain;                             { open files and get options }
 get_date(date,time);                  { get date and time for headings }
 fillchar(dots,sizeof(dots),'.');
 dots[0] := chr(70);                   { set length of dot line }
 search[1] := '{$'+'i';
 search[2] := '{$'+'I';
 search[3] := '(*$'+'i';               { setup search strings for includes }
 search[4] := '(*$'+'I';
 page_no := 0;
 headings;
 xref_head := nil;
end; {initialize}
{
 PROCESS_FILE - print each line of the input file and INCLUDED files,
 if requested.  Create cross-reference records for each variable
 if requested.
}
procedure process_file;
var
 linebuffer    : strptr;
 line_no,                              { current line number in input file }
 include_line  : integer;              { line number in include file }

 including,                            { TRUE = processing include file }
 quote         : boolean;              { quote flag }
 comment_type  : byte;                 { type of comment being processed:
                                          0 = no comment
                                          1 = '{'-type comment
                                          2 = '(*'-type comment }

{ INCLUDEIN - return TRUE if there is an INCLUDE statement in the current line }
function includein(curstr : strptr) : boolean;
var
 x,
 column        : byte;
begin
 x := 0;
 column := 0;
 repeat
   x := x+1;
   column := pos(search[x],curstr^);
 until (x = 4) or (column > 0);
 if (column = 0) then
   includein := false
 else
   includein := not (curstr^[column+length(search[x])] in ['-','+']);
end; {includein}

{ PROCESS_LINE - write PRINTSTR to the output file, updating work_line.
                If cross-referencing, generate XREF records for each
                item found in PRINTSTR }
procedure process_line(printstr : strptr; var work_line : integer);
var
 x             : byte;

{ XREF_LINE - create reference records for each item found in PRINTSTR }
procedure xref_line;
var
 x             : byte;
 wkstr         : string255;
 ch            : char;

{
 ADD_TREE - add a reference to the tree.  If WKSTR is not in the tree,
 create a new node for it.
}
procedure add_tree(var tree : itmptr);
var
 q,p           : itmptr;
 less,
 found         : boolean;

{ MAKETREE - create a new tree node. }
function maketree : itmptr;
var
 p             : itmptr;
begin {maketree}
 new(p);
 with p^ do
   begin
     getmem(idname,length(wkstr)+1);   { allocate just enough for IDNAME }
     idname^ := wkstr;
     if (length(idname^) < max_id_len) then
       numrefs := 0
     else
       numrefs := refs_per_line;
     left := nil;
     right := nil;
     rthrd := false;
     nxtptr := nil;                    { set reference pointer }
     fstptr := nil;
   end;
 maketree := p;
end; {maketree}

procedure setleft(p : itmptr);
var
 q             : itmptr;
begin {setleft}
 q := maketree;
 p^.left := q;
 q^.right := p;                        { inorder successor of q is p }
 q^.rthrd := true;
end; {setleft}

procedure setright(p : itmptr);
var
 q             : itmptr;
begin {setright}
 q := maketree;
 q^.right := p^.right;                 { inorder successor of q is successor of p }
 q^.rthrd := p^.rthrd;                 { may or may not be thread pointer }
 p^.right := q;
 p^.rthrd := false;
end; {setright}

procedure add_ref(p : itmptr; line_no,include_line : integer);
var
 r             : refptr;
begin {add_ref}
 new(r);                               { create a new reference record }
 with r^ do
   begin
     line := line_no;
     incl := include_line;
     nxtptr := nil;
   end;
 with p^ do
   begin
     if (fstptr = nil) then            { if first reference for this record }
       fstptr := r                     { setup list head pointer }
     else
       nxtptr^.nxtptr := r;            { link previous last ref to new }
     nxtptr := r;                      { point to last }
     if (include_line > 0) then        { update reference counter }
       numrefs := numrefs+2            { INCLUDEs take 2 spaces }
     else
       numrefs := numrefs+1;
   end;
end; {add_ref}

begin {add_tree}
 if tree = nil then
   begin                               { nothing in the tree }
     tree := maketree;                 { so we'll make it }
     p := tree;
   end
 else
   begin
     q := tree;
     p := tree;
     found := false;
     while (q <> nil) and not found do     { search the tree }
       begin
         p := q;
         if (p^.idname^ = wkstr) then
           found := true                   { found it }
         else
           begin
             less := (wkstr < p^.idname^);
             if (less) t
hen
               q := p^.left
             else
             if (p^.rthrd) then
               q := nil
             else
               q := p^.right;
           end;
       end;
     if (not found) then               { not found, create a new node }
       if (less) then
         begin
           setleft(p);
           p := p^.left;
         end
       else
         begin
           setright(p);
           p := p^.right;
         end;
   end;
 add_ref(p,line_no,include_line);      { create a new reference record }
end; {add_tree}

{ GETCHR - get the next character in the line.  Return 0 at end of line }
procedure getchr;
begin
 if (x = 0) or (x > length(printstr^)) then
   x := 0                              { end of line }
 else
   begin
     ch := upcase(printstr^[x]);       { convert to uppercase for xref }
     x := x+1;
   end;
end;

{ KEYWORD - return TRUE if WKSTR is in the key word table.
           This is a simple binary search }
function keyword : boolean;
const
 nkwords       = 44;                   { number of key words in table }
type
 key_word_table= array[1..nkwords] of string[9];
const
 key_words     : key_word_table =
                 ('ABSOLUTE' ,'AND'      ,'ARRAY'    ,'BEGIN',
                  'CASE'     ,'CONST'    ,'DIV'      ,'DO',
                  'DOWNTO'   ,'ELSE'     ,'END'      ,'EXTERNAL',
                  'FILE'     ,'FOR'      ,'FORWARD'  ,'FUNCTION',
                  'GOTO'     ,'IF'       ,'IN'       ,'INLINE',
                  'LABEL'    ,'MOD'      ,'NIL'      ,'NOT',
                  'OF'       ,'OR'       ,'OVERLAY'  ,'PACKED',
                  'PROCEDURE','PROGRAM'  ,'RECORD'   ,'REPEAT',
                  'SET'      ,'SHL'      ,'SHR'      ,'STRING',
                  'THEN'     ,'TO'       ,'TYPE'     ,'UNTIL',
                  'VAR'      ,'WHILE'    ,'WITH'     ,'XOR');
var
 high,
 low,
 mid           : byte;
begin
 high := nkwords;
 low := 1;
 while (low <= high) do
   begin
     mid := (high+low) div 2;
     if (key_words[mid] = wkstr) then
       begin
         keyword := true;
         exit;
       end
     else
     if (key_words[mid] > wkstr) then
       high := mid-1
     else
       low := mid+1;
   end;
 keyword := false;
end;

begin {xref_line}
 x := 1;                               { start at beginning }
 wkstr := '';
 getchr;
 while (x > 0) do                      { while not end of line }
   begin
     if (ch = '''') and (comment_type = 0) then { set quote flag }
       quote := not(quote)
     else
     if not quote then                 { if not in quote then go }
       case comment_type of
         0 : if ch = '{' then
               comment_type := 1       { start a comment }
             else
             if ch = '(' then
               begin
                 getchr;
                 if (x > 0) then
                   if (ch = '*') then
                     comment_type := 2 { start a comment }
                   else
                     x := x-1;
               end
             else
             if ch in ['A'..'Z'] then  { start a word }
               begin
                 repeat
                   wkstr := wkstr+ch;
                   getchr;
                 until (not (ch in ['0'..'9','A'..'Z','_'])) or (x = 0);
                 if not keyword then   { check for keyword }
                   add_tree(xref_head);{ not keyword, add to xref tree }
                 wkstr := '';
                 if x > 0 then         { if not end of line }
                   x := x-1;           { go back to previous character }
               end;
         1 : if ch = '}' then          { end comment }
               comment_type := 0;
         2 : if ch = '*' then
               begin
                 getchr;
                 if (x > 0) then
                   if (ch = ')') then
                     comment_type := 0 { end comment }
                   else
                     x := x-1;
               end;
       end; { case }
     getchr;
   end; { while }
end; {xref_line}

{ FINDSPACE - find end of last full word that will fit on the line }
function findspace(printstr : strptr; var x : byte) : byte;
var
 y             : byte;
begin
 y := x;
 x := x+printwidth;
 if (x > length(printstr^)) then       { the whole line will fit }
   x := length(printstr^)+1
 else
   begin
     while (printstr^[x] <> ' ') and (x > y) do { look back for first space }
       x := x-1;
     if (x > y) then                   { found it }
       x := x+1
     else
       x := y+printwidth+1;            { no space, break in middle of word }
   end;
 findspace := x-1;
end; {findspace}

{ DETAB - replace all tabs in the line with appropriate number of spaces }
procedure detab(var printstr : string255);
type
 string8       = string[8];
const
 tab           = ^I;
 tab_string    : string8 = '        ';
var
 x             : byte;
begin
 x := pos(tab,printstr);
 while (x > 0) do
   begin
     delete(printstr,x,1);            { remove the tab }
     insert(copy(tab_string,1,8-((x-1) mod 8)),printstr,x); { insert spaces }
     x := pos(tab,printstr);
   end;
end; {detab}

begin {process_line}
 detab(printstr^);
 currow := currow + ((length(printstr^)-1) div printwidth) + 1;
 if currow > printlength then
   begin
     headings;
     currow := currow + ((length(printstr^)-1) div printwidth) + 1;
   end;
 work_line := work_line + 1;
 if including then
   write(outfile,'<',work_line:5,'> : ')
 else
   write(outfile,' ',work_line:5,'  : ');
 x := 1;
 writeln(outfile,copy(printstr^,1,findspace(printstr,x)));
 while x <= length(printstr^) do
   writeln(outfile,' ':10,copy(printstr^,x,findspace(printstr,x)));
 if xref then
   xref_line;
end; {process_line}

procedure process_include_file(incstr : strptr);
var
 namestart,
 nameend       : integer;
 includefile   : text;
 includefilename : filename;

function parse(incstr : strptr) : filename;
begin
 namestart := pos('$I',incstr^)+2;
 if namestart = 2 then
   namestart := pos('$i',incstr^)+2;
 while (incstr^[namestart] = ' ') do
   namestart := namestart + 1;
 nameend := namestart;
 while (not (incstr^[nameend] in [' ','}','*']))
        and ((nameend - namestart) <= pathlength) do
   nameend := nameend + 1;
 nameend := nameend - 1;
 parse := copy(incstr^,namestart,(nameend-namestart+1));
end; {parse}

begin  {process_include_file}
 includefilename := parse(incstr);
 if (pos('.',includefilename) = 0) then
   includefilename := includefilename + '.PAS';
 including := true;
 include_line := 0;
 if not open(includefile,includefilename) then
   begin
     linebuffer^ := 'ERROR -- Include file not found:  ' + includefilename;
     process_line(linebuffer,include_line);
   end
 else
   begin
     while not eof(includefile) do
       begin
         readln(includefile,linebuffer^);
         process_line(linebuffer,include_line);
       end;
     close(includefile);
   end;
 including := false;
 include_line := 0;
end; {process_include_file}

begin  {process_file}
 new(linebuffer);
 quote := false;
 comment_type := 0;
 line_no := 0;
 include_line := 0;
 including := false;                   { not including a file now }
 while not eof(mainfile) do
   begin
     readln(mainfile,linebuffer^);
     process_line(linebuffer,line_no);
     if includes and includein(linebuffer) then
       process_include_file(linebuffer);
   end;
 dispose(linebuffer);
end; {process_file}

{ PRINT_XREF - print the cross-reference listing }
procedure print_xref(xref_head : itmptr);
var
 ref_count     : integer;
 p,q           : itmptr;

{ LPWRITELN - write a newline on output file.  Check for page break. }
procedure lpwriteln;
begin
 if (currow > printlength) then
   headings;                           { new page }
 writeln(outfile);
 currow := currow + 1;
end;

{ NEWLINE - need another line for references.  Start at position (MAX_ID_LEN+1) }
procedure newline;
begin
 lpwriteln;
 write(outfile,' ':(max_id_len + 1));
 ref_count := 1;
end;

{ PRINT_REFS - Print the list of references for the current node. }
procedure print_refs(node : itmptr);
var
 list          : refptr;

{ WRITE_REF - output one reference to the print file }
procedure write_ref(ref : refptr);
var
 inclstr       : string8;
 inclen        : byte absolute inclstr; {easier than length(inclstr)}
begin
 with ref^ do
   begin
     if (ref_count > refs_per_line) then
       newline;
     write(outfile,line:1);
     if (incl = 0) then
       begin                           { no include in this reference }
         str(line:1,inclstr);
         if (inclen < 6) then
           write(outfile,' ':(6-inclen));
         ref_count := ref_count + 1;
       end
     else
       begin                           { process INCLUDEd reference }
         write(outfile,'<',incl:1,'>');
         str(line:1,inclstr);
         if (inclen < 6) then
           write(outfile,' ':(6-inclen));
         str(incl:1,inclstr);
         if (inclen < 4) then
           write(outfile,' ':(4-inclen));
         ref_count := ref_count + 2;
       end;
   end; {with}
end; {write_ref}

begin {print_refs}
 if ((node^.numrefs div refs_per_line) > (printlength - currow)) then
   headings;
 write(outfile,node^.idname^);         { output idname }
 if (length(node^.idname^) >= max_id_len) then
   newline
 else
   write(outfile,' ':(max_id_len-length(node^.idname^)+1));

 ref_count := 1;
 list := node^.fstptr;
 repeat
   write_ref(list);
   list := list^.nxtptr;
 until (list = nil);
 lpwriteln;
end; {print_refs}

{ in-order traversal of a right in-threaded binary tree. }
begin {print_xref}
 headings;
 p := xref_head;
 repeat
   q := nil;
   while (p <> nil) do
     begin                             { traverse left branch }
       q := p;
       p := p^.left;
     end;
   if (q <> nil) then
     begin
       print_refs(q);
       p := q^.right;
       while (q^.rthrd) do
         begin                         { back up }
           print_refs(p);
           q := p;
           p := p^.right;
         end;
     end;
 until (q = nil);
end; {print_xref}

begin { typex }
 writeln('[TYPEX Version ',version_no,']');
 initialize;
 process_file;
 if xref then
   print_xref(xref_head);
 page(outfile);
 close(mainfile);
 close(outfile);
end. { typex }