-- DFParserImpl.Mesa
-- last edit December 29, 1982 3:24 pm
-- last edit May 22, 1983 1:16 pm, Russ Atkinson
-- Pilot 6.0/ Mesa 7.0

DIRECTORY
CWF: TYPE USING [WF0, WF1, WF2, WF3, WF4],
Date: TYPE USING [StringToPacked],
DFSubr: TYPE USING [AppendToUsingSeq, Criterion, DF, DFEntryProcType, DFFileRecord, DFSeq,
 FreeUsingSeq, InterestingNestedDFProcType, NextDF,
 StripLongName, UsingEmpty, UsingSeq, ZoneType],
IO: TYPE USING[UserAbort],
LongString: TYPE USING [AppendChar, AppendString, EquivalentString, InvalidNumber],
Stream: TYPE USING [EndOfStream, GetChar, Handle],
Subr: TYPE USING [AbortMyself, AllocateString, CopyString, EndsIn, errorflg,
 FreeString, LongZone, strcpy, SubStrCopy, TTYProcs],
Time: TYPE USING [Invalid];

DFParserImpl: PROGRAM
IMPORTS CWF, Date, DFSubr, IO, LongString, Stream, Subr, Time
EXPORTS DFSubr = {

-- MDS usage (encouraged so that we won't run out of Resident VM)
-- this is a constant array
tokenString: ARRAY Token OF STRING = [
 "bad"L, "CameFrom"L, "Directory"L, "Exports"L,
 "Host"L, "Imports"L, "Include"L, "Of"L, "Public"L,
 "PublicOnly"L, "ReadOnly"L, "ReleaseAs"L, "Using"L,
-- special characters
 "@"L, "{"L, "}"L, "["L, "]"L, "Comment"L,
 "~"L, "+"L, "~="L, ">"L, "filename or date"L, "EOF"L, "CR"L];
-- end of MDS (there are also string literals)

Token: TYPE = {tBad,
-- keywords
 tCameFrom, tDirectory, tExports, tHost, tImports,
 tInclude, tOf, tPublic, tPublicOnly, tReadOnly, tReleaseAs, tUsing,
-- special characters
 tAtSign, tOpenCurly, tCloseCurly, tOpenBracket, tCloseBracket, tComment,
 tTilde, tPlus, tNotEqual, tGreaterThan, tOther, tEOF, tCR};
-- tOther is a catch all that includes filenames
-- such as <dir>schmidt.df; dates, etc.


-- this outer procedure is here to hold state for the inner
-- parsing procedures and avoid using any MDS
-- making the code reentrant

-- if noremoteerrors is true, don't complain if a file doesn't
--  appear to have a remote place
-- if forceReadonly then make every entry in this DF file be ReadOnly
-- if omitNonPublic, then don't parse them into dfseq
-- dffilename is for error messages
-- if using ~= NIL then restrict parsing to those on using list
-- sh may not be a FileStream!

-- dfseq does not need to be local!

-- justexception is here to kludge over bugs in with
-- the spacing on Host and Directory: it is not used with Imports and Includes

ParseStream: PUBLIC PROC[sh: Stream.Handle, dfseq: DFSubr.DFSeq, dffilename: LONG STRING,
 using: DFSubr.UsingSeq, noremoteerrors, forceReadonly, omitNonPublic: BOOL,
 h: Subr.TTYProcs,
 interestingNestedDF: DFSubr.InterestingNestedDFProcType, dfEntryProc: DFSubr.DFEntryProcType,
 ancestor: LONG STRING, nLevel: CARDINAL] = {
peekch: CHAR ← ' ;  -- peeck character
tok: Token ← tBad;  -- next token
wholecomment: LONG STRINGNIL; -- memory in longzone
streamposn: LONG CARDINAL ← 0;
justexception: BOOLFALSE;
longzone: UNCOUNTED ZONE ← Subr.LongZone[];

-- these strings are allocated dynamically to prevent running out of resident VM
-- beware: they are in a longzone
tokenstr: LONG STRINGNIL;  -- receives text of next token (tok)
commentline: LONG STRINGNIL; -- used by GetC to accumulate comments, includes trailing CR
directory: LONG STRINGNIL;  -- the current directory
releasedir: LONG STRINGNIL;  -- the current release directory
fullname: LONG STRINGNIL;  -- the whole name
shortname: LONG STRINGNIL;  -- the short name
tempdir: LONG STRINGNIL;  -- explicit directory on a filename


LittleCopyString: PROC[newstr, oldstr: LONG STRING] RETURNS[LONG STRING] = {
RETURN[IF dfseq.zoneType ~= huge
  OR oldstr = NIL
  OR NOT LongString.EquivalentString[newstr, oldstr] THEN
   Subr.CopyString[newstr, dfseq.dfzone]
  ELSE oldstr];
 };

CopyHost: PROC[host: LONG STRING] RETURNS[LONG STRING] = {
IF dfseq.zoneType = huge THEN {
  IF LongString.EquivalentString[host, dfseq.indigoHost] THEN RETURN[dfseq.indigoHost];
  IF LongString.EquivalentString[host, dfseq.ivyHost] THEN RETURN[dfseq.ivyHost];
  };
RETURN[Subr.CopyString[host, dfseq.dfzone]];
 };

-- does not get the following token
-- parses [host]<directory> or <directory>
-- called only if tok = tOther or tOpenBracket
HostAndOrDir: PROC[host, directory: LONG STRING] = {
IF tok = tOpenBracket THEN { -- [ host ]
  GetToken[TRUE];
  CkTok[tok, tOther];
  Subr.strcpy[host, tokenstr];
  GetToken[TRUE];
  CkTok[tok, tCloseBracket];
  GetToken[TRUE];
  };
-- the next string is the directory, possibly with <>
 CkTok[tok, tOther];
 Subr.strcpy[directory, tokenstr];
IF directory.length = 0 THEN {
  IF NOT noremoteerrors THEN {
   CWF.WF0["Error - Null directory entry.\n"];
   Subr.errorflg ← TRUE;
   };
  RETURN;
  };
IF directory[directory.length - 1] = '> THEN
  directory.length ← directory.length -1;
IF directory[0] = '< THEN
  Subr.SubStrCopy[directory, directory, 1];
 };

-- does not get the following token
ParseFullName: PROC[host, directory, filename: LONG STRING]
  RETURNS[vers: CARDINAL] = {
 host.length ← 0;
IF tok = tOpenBracket THEN {
  GetToken[TRUE];
  CkTok[tok, tOther];
  Subr.strcpy[host, tokenstr];
  GetToken[TRUE];
  CkTok[tok, tCloseBracket];
  GetToken[TRUE];
  };
-- tok should now be <directory>file!vers
 CkTok[tok, tOther];
 vers ← DFSubr.StripLongName[tokenstr, NIL, directory, filename, FALSE
  ! LongString.InvalidNumber => {
   posn: LONG CARDINALIF streamposn = 0 THEN 0
    ELSE streamposn - 1;
   CWF.WF2["Error - invalid number at position %lu in file %s.\n",
    @posn, dffilename];
   SIGNAL Subr.AbortMyself;
   }
  ];
 };

CkTok: PROC[token: Token, shouldbe: Token] = {
IF token ~= shouldbe THEN {
  posn: LONG CARDINALIF streamposn = 0 THEN 0 ELSE streamposn - 1;
  CWF.WF4["Error - expecting a %s, found a %s at position %lu in file %s.\n",
   tokenString[token], tokenString[shouldbe], @posn, dffilename];
  };
};

Misplaced: PROC[token: Token] = {
 posn: LONG CARDINALIF streamposn = 0 THEN 0 ELSE streamposn - 1;
CWF.WF3["Error - was not expecting the token '%s' at position %lu in file %s.\n",
  tokenString[token], @posn, dffilename];
 };

SkipCR: PROC = {
IF tok = tCR THEN
  GetToken[TRUE];
 };

-- gets the following token, first on the next line
GetCR: PROC = {
-- forces next GetToken to the beginning of the line
-- (and not return a CR)
DO
  IF tok = tCR OR tok = tEOF THEN EXIT;
  GetToken[FALSE];
  ENDLOOP;
 GetToken[TRUE];
 };

ParseDateField: PROC RETURNS[criterion: DFSubr.Criterion,
  wantdate: LONG CARDINAL] = {
 sdate: LONG STRINGNIL;
 savetok: Token;
 wantdate ← 0;
 criterion ← none;
 GetToken[FALSE];
IF tok = tOf THEN
  GetToken[FALSE];
IF tok = tCR THEN {
  GetToken[TRUE];
  RETURN;
  };
 savetok ← tBad;
 sdate ← Subr.AllocateString[100];
 {ENABLE UNWIND => {Subr.FreeString[sdate]};
WHILE tok = tOther OR tok = tNotEqual OR tok = tGreaterThan DO
  LongString.AppendString[sdate, tokenstr];
  LongString.AppendChar[sdate, ' ];
  savetok ← tok;
  GetToken[FALSE];
  ENDLOOP;
-- tok may be tBad or tCR or be one of the following
SELECT savetok FROM
 tNotEqual => criterion ← notequal;
 tGreaterThan => criterion ← update;
 tOther => {
  short: STRING ← [32]; -- must be short for Date.StringToPacked, dammitt!
  IF sdate.length > 32 THEN sdate.length ← 32;
  LongString.AppendString[short, sdate];
  wantdate ← Date.StringToPacked[short
  ! Time.Invalid => {
   CWF.WF3["Error - '%s' is an invalid time at position %lu in %s.\n",
    sdate, @streamposn, dffilename];
   wantdate ← 0;
   CONTINUE;
   }];
  };
 tCR => NULL; -- no date given
ENDCASE => CWF.WF3["Error - expecting a date at position %lu in %s, \nfound '%s' instead.\n",
  @streamposn, dffilename, tokenstr];
 }; -- of ENABLE UNWIND
 Subr.FreeString[sdate];
 };

-- this procedure is called ONCE by the outer procedure
ParseStreamInternal: PROC = {
 nExports: CARDINAL;
 readonly, public, camefrom: BOOLFALSE;
 df: DFSubr.DF;
 lastdir, lastreleasedir: LONG STRINGNIL;
 isatsign, istopmark, ispublicOnly, isnoremoteversion, exportsImports: BOOL;
 host: LONG STRING ← Subr.AllocateString[40];  -- the current host
 releaseHost: LONG STRING ← Subr.AllocateString[40]; -- the current release host
 localhost: LONG STRING ← Subr.AllocateString[40];
 localdirectory: LONG STRING ← Subr.AllocateString[100];
 localrhost: LONG STRING ← Subr.AllocateString[40];
 localrdirectory: LONG STRING ← Subr.AllocateString[100];

 {ENABLE UNWIND => {
  Subr.FreeString[host]; Subr.FreeString[releaseHost];
  Subr.FreeString[localhost]; Subr.FreeString[localdirectory];
  Subr.FreeString[localrhost]; Subr.FreeString[localrdirectory];
  };
 nExports ← 0;
 [] ← GetC[]; -- to set up peekch
 GetToken[TRUE]; -- first token
 exportsImports ← FALSE;
DO
  -- tok is from last GetToken[]; should be first on next line
  IF h.in.UserAbort[] THEN SIGNAL Subr.AbortMyself;
  justexception ← FALSE;
  SELECT tok FROM
  tEOF => EXIT;
  tHost => {
   justexception ← TRUE;
   GetToken[TRUE];
   CkTok[tok, tOther];
   Subr.strcpy[host, tokenstr];
   GetCR[]; -- forces new line
   };
  tDirectory, tPublic, tExports, tReadOnly => {
   justexception ← TRUE;
   exportsImports ← public ← readonly ← camefrom ← FALSE;
   DO
    SELECT tok FROM
    tImports => GOTO isImports;
    tReadOnly => readonly ← TRUE;
    tExports, tPublic => public ← TRUE;
    tOpenBracket => EXIT;
    tDirectory => NULL;
    ENDCASE => Misplaced[tok];
    GetToken[TRUE];
    IF tok = tOther OR tok = tEOF OR tok = tOpenBracket THEN
     EXIT;
    ENDLOOP;
   HostAndOrDir[host, directory];
   -- parse CameFrom or ReleaseAs
   GetToken[TRUE];
   releaseHost.length ← releasedir.length ← 0;
   IF tok = tCameFrom OR tok = tReleaseAs THEN {
    camefrom ← (tok = tCameFrom);
    GetToken[TRUE];
    HostAndOrDir[releaseHost, releasedir];
    GetCR[]; -- gets new line & next token
    };
   EXITS
   isImports => exportsImports ← TRUE; -- "Exports Imports", goto tImports processing
   };
  tImports, tInclude => {
   -- if exportsImports is true, then this is an Exports Imports
   innerUsing: DFSubr.UsingSeq ← NIL;
   vers: CARDINAL;
   savecomment: LONG STRINGNIL; -- memory in longzone
   createtime: LONG CARDINAL;
   criterion: DFSubr.Criterion;
   savetok: Token ← tok;
   skipIt, callIt, publicOk: BOOLFALSE;
   -- (Exports) Imports [host]<path>file!vers Of <date> CameFrom [host]<path>
   -- Imports (Exports) [host]<path>file!vers Of <date> CameFrom [host]<path>
   -- Include [host]<path>file!vers Of <date> (CameFrom|ReleaseAs [host]<path>)
   -- Using [ file list ]
   localhost.length ← 0;
   localdirectory.length ← 0;
   localrhost.length ← 0;
   localrdirectory.length ← 0;
   df ← NIL;
   GetToken[TRUE];
   savecomment ← wholecomment;
   wholecomment ← NIL;
   IF savetok = tExports THEN {
    exportsImports ← TRUE;
    GetToken[TRUE]; -- Imports Exports
    };
   vers ← ParseFullName[localhost, localdirectory, shortname];
   skipIt ← FALSE;
   -- skip it if there is a using list and it does not consume an element
   -- or if the Imports is not Public
   -- don't skip it if it is Include
   IF using ~= NIL THEN
    skipIt ← NOT Consume[using, shortname]
   ELSE skipIt ← (omitNonPublic AND NOT exportsImports AND savetok = tImports);
   IF exportsImports THEN nExports ← nExports + 1;
   IF NOT skipIt THEN {
    df ← DFSubr.NextDF[dfseq];
    IF df = NIL THEN EXIT;
    df.host ← CopyHost[localhost];
    df.directory ← Subr.CopyString[localdirectory, dfseq.dfzone];
    df.shortname ← Subr.CopyString[shortname, dfseq.dfzone];
    IF savecomment ~= NIL THEN
     df.comment ← Subr.CopyString[savecomment, dfseq.dfzone];
    };
   [criterion, createtime] ← ParseDateField[];
   SkipCR[];
   camefrom ← FALSE;
   IF tok = tCameFrom OR tok = tReleaseAs THEN {
    camefrom ← (tok = tCameFrom);
    GetToken[TRUE];
    HostAndOrDir[localrhost, localrdirectory];
    GetToken[TRUE];
    };
   IF NOT skipIt THEN {
    IF savetok = tImports THEN
     df.readonly ← df.publicOnly ← df.atsign ← TRUE
    ELSE -- tInclude --
     df.atsign ← TRUE;
    df.version ← vers;
    df.public ← exportsImports; -- from Exports Imports
    df.criterion ← criterion;
    df.createtime ← createtime;
     df.releaseDirectory ← IF localrdirectory.length > 0 THEN
     Subr.CopyString[localrdirectory, dfseq.dfzone] ELSE NIL;
    df.releaseHost ← IF localrhost.length > 0 THEN CopyHost[localrhost] ELSE NIL;
    df.cameFrom ← camefrom;
    };
   IF tok = tUsing AND savetok = tImports THEN {
    GetToken[TRUE];
    CkTok[tok, tOpenBracket];
    DO
     GetToken[TRUE];
     IF tok = tCloseBracket OR tok = tEOF THEN EXIT;
     CkTok[tok, tOther];
     innerUsing ← DFSubr.AppendToUsingSeq[innerUsing, tokenstr,
       dfseq.dfzone];
     ENDLOOP;
    IF NOT skipIt THEN df.using ← innerUsing;
    GetCR[];
    };
   IF savecomment ~= NIL THEN
    Subr.FreeString[savecomment, longzone];
   savecomment ← NIL;
   -- first call about this entry
   IF NOT skipIt AND dfEntryProc ~= NIL THEN
    dfEntryProc[dfEntry: df];
   -- call the user about this DF file
   -- call if
   -- 1) the using list we are driven by is not exhausted and
   --  a) there is an inner using list so we must check the intersection
   --   b) if we are looking at an Imports w/o Using list then this entry is Public,
   --  c) if this is an Included DF file
   -- 2) or if the using list is exhausted, the DF file was on the using list;
   -- 3) or if there is no using list and
   --  a) the DF file is Public
   --   b) if PublicOnly is false
   --  c) if the df file is an Includes
   publicOk ← exportsImports OR NOT omitNonPublic;
   callIt ← interestingNestedDF ~= NIL;
   IF using ~= NIL THEN
    callIt ← callIt
    AND ((NOT DFSubr.UsingEmpty[using]
       AND (savetok = tInclude OR publicOk OR innerUsing ~= NIL))
     OR NOT skipIt)
   ELSE callIt ← callIt AND (publicOk OR savetok = tInclude);
   IF callIt THEN
    interestingNestedDF[host: localhost, directory: localdirectory, shortname: shortname,
     ancestor: ancestor, immediateParent: dffilename, nLevel: nLevel,
     version: vers, createtime: createtime,
     driverUsingSeq: using, innerUsingSeq: innerUsing, dfEntry: df,
     entryIsReadonly: (savetok = tImports) OR forceReadonly,
     publicOnly: (savetok = tImports) OR omitNonPublic,
     criterion: criterion];
   IF skipIt THEN DFSubr.FreeUsingSeq[innerUsing];
   exportsImports ← public ← FALSE; -- reset for next line
   };
  tOther, tAtSign, tPlus, tTilde => {
   criterion: DFSubr.Criterion;
   createtime: LONG CARDINAL;
   skipIt, callIt, publicOk, isNewOnly: BOOL;
   vers: CARDINAL;
   -- +, ~, @ { PublicOnly } <Directory>Filename!vers createdate
  
   df ← NIL;
   skipIt ← FALSE;
   -- if no using list and this is exports only, then skip it
   IF omitNonPublic AND NOT public AND using = NIL THEN
    skipIt ← TRUE;
   IF public THEN nExports ← nExports + 1;
   isatsign ← istopmark ← ispublicOnly ← isNewOnly ← FALSE;
   DO
    SELECT tok FROM
    tAtSign => isatsign ← TRUE;
    tOpenCurly => NULL;
    tPublicOnly => ispublicOnly ← TRUE;
    tCloseCurly => NULL;
    tPlus => istopmark ← TRUE;
    tTilde => isNewOnly ← TRUE;
    tOther, tEOF => EXIT;
    ENDCASE => Misplaced[tok];
    GetToken[TRUE];
    ENDLOOP;
   IF tok = tEOF THEN LOOP;
   CkTok[tok, tOther];
   isnoremoteversion ← FALSE;
   Subr.strcpy[fullname, tokenstr];
   IF tokenstr[0] ~= '< AND directory.length = 0 THEN {
    IF NOT noremoteerrors THEN {
     Subr.errorflg ← TRUE;
     CWF.WF1[
    "Error - directory not specified for '%s'.\n", tokenstr];
     };
    isnoremoteversion ← TRUE;
    };
   -- fullname looks like <schmidt>model>junk.mesa!3 or junk.mesa!3
   vers ← DFSubr.StripLongName[fullname, NIL, tempdir,
    shortname, FALSE
    ! LongString.InvalidNumber => {
     posn: LONG CARDINALIF streamposn = 0 THEN 0
      ELSE streamposn - 1;
     CWF.WF2["Error - invalid number at position %lu in file %s.\n",
      @posn, dffilename];
      SIGNAL Subr.AbortMyself;
      }
     ];
   -- skip it if not on using list
   -- we will not skip DF files that are referenced in included DF files (omitNonPublic = FALSE),
   -- we will skip DF files that are referenced in imported DF files (omitNonPublic = TRUE)
   IF using ~= NIL THEN
    skipIt ← NOT Consume[using, shortname];
   -- skip it if there is a using list and it does not consume an element
   IF NOT skipIt THEN {
    df ← DFSubr.NextDF[dfseq];
    IF df = NIL THEN EXIT;
    -- get previous comment saved
    IF wholecomment ~= NIL THEN
     df.comment ← Subr.CopyString[wholecomment, dfseq.dfzone];
    };
   -- free previous comment at this point
   -- any later and the parsedatefield/skipCR will loose a line
   IF wholecomment ~= NIL THEN
    Subr.FreeString[wholecomment, longzone];
   wholecomment ← NIL;
   [criterion, createtime] ← ParseDateField[];
   SkipCR[]; -- if we are looking at a CR, get next tok
   IF NOT skipIt THEN {
    lastdir ← df.directory ← IF isnoremoteversion THEN NIL
     ELSE IF tempdir.length = 0 THEN LittleCopyString[directory, lastdir]
     ELSE LittleCopyString[tempdir, lastdir];
    lastreleasedir ← df.releaseDirectory ← IF releasedir.length > 0 THEN
     LittleCopyString[releasedir, lastreleasedir] ELSE NIL;
    df.releaseHost ← IF releaseHost.length > 0 THEN CopyHost[releaseHost] ELSE NIL;
    df.host ← CopyHost[host];
    df.shortname ← Subr.CopyString[shortname, dfseq.dfzone];
    df.version ← vers;
    df.atsign ← isatsign;
    df.topmark ← istopmark;
    df.newOnly ← isNewOnly;
    df.readonly ← forceReadonly OR readonly;
    df.public ← public;
    df.publicOnly ← ispublicOnly;
    df.cameFrom ← camefrom;
    df.createtime ← createtime;
    df.criterion ← criterion;
    };
   -- first call about this entry
   IF NOT skipIt AND dfEntryProc ~= NIL THEN
    dfEntryProc[dfEntry: df];

   -- call the user about this DF file
   publicOk ← public OR NOT omitNonPublic;
   callIt ← isatsign AND interestingNestedDF ~= NIL AND Subr.EndsIn[shortname, ".df"];
   IF using ~= NIL THEN
    callIt ← callIt
     AND ((NOT DFSubr.UsingEmpty[using] AND publicOk OR NOT readonly)
      OR NOT skipIt)
   ELSE callIt ← callIt AND (publicOk OR NOT readonly);
   IF callIt THEN
    interestingNestedDF[host: host, directory: directory, shortname: shortname,
     ancestor: ancestor, immediateParent: dffilename, nLevel: nLevel,
     version: vers, createtime: createtime,
     driverUsingSeq: using, innerUsingSeq: NIL, dfEntry: df,
     entryIsReadonly: readonly OR forceReadonly,
     publicOnly: ispublicOnly OR omitNonPublic,
     criterion: criterion];
   };
  ENDCASE => {
   Misplaced[tok];
   GetToken[TRUE];
   };
  ENDLOOP;
IF wholecomment ~= NIL THEN {
  IF dfseq.trailingcomment = NIL THEN
   dfseq.trailingcomment ← Subr.CopyString[wholecomment, dfseq.dfzone];
  Subr.FreeString[wholecomment, longzone];
  };
 }; -- of ENABLE UNWIND
 Subr.FreeString[host]; Subr.FreeString[releaseHost];
 Subr.FreeString[localhost]; Subr.FreeString[localdirectory];
 Subr.FreeString[localrhost]; Subr.FreeString[localrdirectory];
-- don't give warning if no exports, since it may be nested Includes
-- IF omitNonPublic AND nExports = 0 THEN
  -- CWF.WF1["Warning - %s is analyzed Exports Only, but it has no Exports!\n", dffilename];
 }; -- end of ParseStreamInternal

-- returns results in tok and tokenstr
GetToken: PROC[ignoreLeadingCR: BOOL] = {
 ch: CHAR;
 tok ← tBad;
 tokenstr.length ← 0;
WHILE peekch = ' OR peekch = '\t OR peekch = ',
OR peekch = '/ OR peekch = '-   -- questionable?
OR (ignoreLeadingCR AND peekch = '\n) DO
  [] ← GetC[];
  ENDLOOP;
IF peekch IN ['A .. 'Z] OR peekch IN ['a .. 'z]
OR peekch IN ['0 .. '9] OR peekch = '< THEN {
  tok ← tOther;
  DO
   ch ← GetC[];
   IF tokenstr.length >= tokenstr.maxlength THEN {
    CWF.WF1["String '%s' is too long.\n", tokenstr];
    RETURN;
    };
   tokenstr[tokenstr.length] ← ch;
   tokenstr.length ← tokenstr.length + 1;
   -- not ">", "-" as they are part of file names
   IF peekch = 0C
   OR peekch = '[
   OR peekch = ']
   OR peekch = '
   OR peekch = '\t
   OR peekch = '\n
   OR peekch = ',
   OR peekch = '+
   OR peekch = '@
   OR peekch = '{
   OR peekch = '}
   OR peekch = '/
   OR peekch = '~
   THEN
    EXIT;
   ENDLOOP;
  --now see if reserved or identifier or other
  IF tokenstr[0] = '< OR tokenstr.length > 10 THEN RETURN;
  -- claim: this is as good as hashing!!
  SELECT tokenstr.length FROM
  2 =>
   IF LongString.EquivalentString[tokenstr, "of"] THEN
    tok ← tOf;
  4 =>
   IF LongString.EquivalentString[tokenstr, "host"] THEN
    tok ← tHost;
  5 =>
   IF LongString.EquivalentString[tokenstr, "using"] THEN
    tok ← tUsing;
  6 =>
   IF LongString.EquivalentString[tokenstr, "public"] THEN
    tok ← tPublic;
  7 => {
   IF LongString.EquivalentString[tokenstr, "exports"] THEN
    tok ← tExports;
   IF LongString.EquivalentString[tokenstr, "imports"] THEN
    tok ← tImports;
   IF LongString.EquivalentString[tokenstr, "include"] THEN
    tok ← tInclude;
   };
  8 => {
   IF LongString.EquivalentString[tokenstr, "includes"] THEN
    tok ← tInclude;
   IF LongString.EquivalentString[tokenstr, "camefrom"] THEN
    tok ← tCameFrom;
   IF LongString.EquivalentString[tokenstr, "readonly"] THEN
    tok ← tReadOnly;
   };
  9 => {
   IF LongString.EquivalentString[tokenstr, "directory"] THEN
    tok ← tDirectory;
   IF LongString.EquivalentString[tokenstr, "releaseas"] THEN
    tok ← tReleaseAs;
   };
  10 =>
   IF LongString.EquivalentString[tokenstr, "publiconly"] THEN
    tok ← tPublicOnly;
  -- don't change # 10 without looking at IF test above
  ENDCASE;
  RETURN;
  };
 ch ← GetC[];
 LongString.AppendChar[tokenstr, ch];
-- not alphanumeric, look for magic characters
SELECT ch FROM
-- one char delimiters
 '\n => tok ← tCR;
 '+ => tok ← tPlus;
 '@ => tok ← tAtSign;
 '{ => tok ← tOpenCurly;
 '} => tok ← tCloseCurly;
 '[ => tok ← tOpenBracket;
 '] => tok ← tCloseBracket;
 '# => tok ← tNotEqual;
 '> => tok ← tGreaterThan;
 0C => tok ← tEOF;
-- two chars
 '~ => {
  IF peekch = '= THEN {
   [] ← GetC[];
   tok ← tNotEqual;
   }
  ELSE tok ← tTilde;
  };
ENDCASE => tok ← tOther;
 };

ReadC: PROC = {
 peekch ← Stream.GetChar[sh
  ! Stream.EndOfStream => {
   peekch ← 0C;
   CONTINUE;
   }
  ];
 streamposn ← streamposn + 1;
 };

GetC: PROC RETURNS[ch: CHAR] = {
 ch ← peekch;
IF ch = 0C THEN RETURN; -- STP only signals EndOfStream once, then hangs
 ReadC[];
DO
  -- strips leading blanks from lines
  IF ch = '\n AND peekch = ' THEN {
   WHILE peekch = ' DO
    ReadC[];
    ENDLOOP;
   -- note that ch remains = \n
   };
  IF (ch = '/ AND peekch = '/)
  OR (ch = '- AND peekch = '-)
  OR (ch = '\n AND peekch = '\n AND NOT justexception) THEN {
   -- handle comment
   len: CARDINAL;
   savecomment: LONG STRING;
   commentline.length ← 0;
   LongString.AppendChar[commentline, ch];
   IF peekch ~= '\n THEN {
    -- get rest of comment line
    DO
     LongString.AppendChar[commentline, peekch];
     ReadC[];
     IF peekch = '\n OR peekch = 0C THEN EXIT;
     ENDLOOP;
    LongString.AppendChar[commentline, peekch];
    };
   ch ← peekch;
   IF ch ~= 0C THEN ReadC[];
   len ← commentline.length;
   IF wholecomment ~= NIL THEN len ← len + wholecomment.length;
   savecomment ← wholecomment;
   wholecomment ← Subr.AllocateString[len, longzone];
   IF savecomment ~= NIL THEN {
    Subr.strcpy[wholecomment, savecomment];
    Subr.FreeString[savecomment, longzone];
    };
   LongString.AppendString[wholecomment, commentline];
   -- CWF.WF1["Comment !%s!\n", wholecomment];
   }
  ELSE EXIT;
  ENDLOOP;
 };

Cleanup: PROC = {
 Subr.FreeString[tempdir, longzone];
 Subr.FreeString[shortname, longzone];
 Subr.FreeString[fullname, longzone];
 Subr.FreeString[releasedir, longzone];
 Subr.FreeString[directory, longzone];
 Subr.FreeString[tokenstr, longzone];
 Subr.FreeString[commentline, longzone];
 };

-- these strings are allocated here to avoid running out of resident VM
commentline ← Subr.AllocateString[600, longzone];
tokenstr ← Subr.AllocateString[100, longzone];
directory ← Subr.AllocateString[100, longzone];
releasedir ← Subr.AllocateString[100, longzone];
fullname ← Subr.AllocateString[100, longzone];
shortname ← Subr.AllocateString[100, longzone];
tempdir ← Subr.AllocateString[100, longzone];
ParseStreamInternal[
 ! UNWIND => Cleanup[]];
Cleanup[];
};

Consume: PROC[usingseq: DFSubr.UsingSeq, shortname: LONG STRING] RETURNS[consumed: BOOL] = {
FOR i: CARDINAL IN [0 .. usingseq.size) DO
IF usingseq[i] = NIL THEN LOOP;
IF LongString.EquivalentString[usingseq[i], shortname] THEN {
  Subr.FreeString[usingseq[i], usingseq.zone];
  usingseq[i] ← NIL;
  RETURN[TRUE];
  };
ENDLOOP;
RETURN[FALSE];
};

}.