-- DFParserImpl.Mesa, last edit December 29, 1982 3:24 pm
-- 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],
  Stream: TYPE USING [EndOfStream, GetChar, Handle],
  String: TYPE USING [AppendChar, InvalidNumber],
  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, String, 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 STRING ← NIL;	-- memory in longzone
streamposn: LONG CARDINAL ← 0;
justexception: BOOL ← FALSE;
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 STRING ← NIL;		-- receives text of next token (tok)
commentline: LONG STRING ← NIL;	-- used by GetC to accumulate comments, includes trailing CR
directory: LONG STRING ← NIL;		-- the current directory
releasedir: LONG STRING ← NIL;		-- the current release directory
fullname: LONG STRING ← NIL;		-- the whole name
shortname: LONG STRING ← NIL;		-- the short name
tempdir: LONG STRING ← NIL;		-- 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
		! String.InvalidNumber => {
			posn: LONG CARDINAL ← IF 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 CARDINAL ← IF 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 CARDINAL ← IF 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: STRING ← [100];
	savetok: Token;
	wantdate ← 0;
	criterion ← none;
	GetToken[FALSE];
	IF tok = tOf THEN 
		GetToken[FALSE];
	IF tok = tCR THEN {
		GetToken[TRUE];
		RETURN;
		};
	savetok ← tBad;
	WHILE tok = tOther OR tok = tNotEqual OR tok = tGreaterThan DO
		LongString.AppendString[sdate, tokenstr];
		String.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 => {
		wantdate ← Date.StringToPacked[sdate
		! 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];
	};

	-- this procedure is called ONCE by the outer procedure
	ParseStreamInternal: PROC = {
	host: STRING ← [30];		-- the current host
	releaseHost: STRING ← [30];	-- the current release host
	nExports: CARDINAL;
	readonly, public, camefrom: BOOL ← FALSE;
	df: DFSubr.DF;
	lastdir, lastreleasedir: LONG STRING ← NIL;
	isatsign, istopmark, ispublicOnly, isnoremoteversion, exportsImports: BOOL;

	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 STRING ← NIL;	-- memory in longzone
			createtime: LONG CARDINAL;
			criterion: DFSubr.Criterion;
			savetok: Token ← tok;
			localhost: STRING ← [30];
			localdirectory: STRING ← [100];
			localrhost: STRING ← [30];
			localrdirectory: STRING ← [100];
			skipIt, callIt, publicOk: BOOL ← FALSE;
			-- (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 ]
			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
				! String.InvalidNumber => {
					posn: LONG CARDINAL ← IF 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];
		};
	-- 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];
};

}.