suplib/src/RCSFile.m3


 Copyright 1996-2003 John D. Polstra.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgment:
 *      This product includes software developed by John D. Polstra.
 * 4. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: RCSFile.m3.html,v 1.3 2010-04-29 17:20:02 wagner Exp $ 

UNSAFE MODULE RCSFile;

IMPORT
  CText, FileAttr, Fmt, MD5, OSError, Pathname,
  RCSAccess, RCSAccessList, RCSDate, RCSDelta,
  RCSDeltaClass, RCSDeltaList, RCSDeltaListSort, RCSDeltaTbl,
  RCSError, RCSKeyword, RCSPhrase, RCSPhrases, RCSRevNum, RCSString,
  RCSTag, RCSTagList, RCSTagListSort, SortedRCSDeltaTbl, Text,
  TextIntTbl, TextSeq, Thread, TokScan, UnixMisc, Ustat, Word, Wr;

REVEAL
  T = Public BRANDED OBJECT
    attr: FileAttr.T := NIL;
    buf: ADDRESS := NIL;
    len: CARDINAL := 0;

    deltaTbl: SortedRCSDeltaTbl.T;
    accessList: RCSAccessList.T := NIL;
    tagList: RCSTagList.T := NIL;

    newPhrases: RCSPhrases.T := NIL;

    start: UNTRACED REF CHAR := NIL;
    ptr: UNTRACED REF CHAR := NIL;
    limit: UNTRACED REF CHAR := NIL;

    line: CARDINAL := 1;
    curTok: Token;

    head: RCSDelta.T := NIL;
    tail: RCSDelta.T := NIL;
  OVERRIDES
    init := Init;
  END;

VAR
  keyTab := NEW(TextIntTbl.Default).init(2*NUMBER(Keyword));

PROCEDURE CalculateMD5(rf: T; md5: MD5.T) =
  BEGIN
    md5.updateRaw(rf.buf, rf.len);
  END CalculateMD5;

PROCEDURE Close(rf: T) RAISES {OSError.E} =
  BEGIN
    IF rf.buf # NIL THEN
      UnixMisc.Unmap(rf.buf, rf.len);
      rf.buf := NIL;
    END;
    rf.len := 0;
    rf.start := NIL;
    rf.ptr := NIL;
    rf.limit := NIL;
  END Close;

PROCEDURE GetDelta(rf: T; revNum: RCSRevNum.T): RCSDelta.T
  RAISES {RCSError.E} =
  VAR
    delta: RCSDelta.T;
  BEGIN
    IF NOT rf.deltaTbl.get(revNum, delta) THEN
      Oops(rf, "Non-existent revision number " & revNum);
    END;
    RETURN delta;
  END GetDelta;

PROCEDURE GetHeadDelta(rf: T): RCSDelta.T
  RAISES {RCSError.E} =
  BEGIN
    IF rf.head = NIL THEN
      Oops(rf, "File contains no deltas");
    END;
    RETURN rf.head;
  END GetHeadDelta;

PROCEDURE GetTagDelta(rf: T;
                      tag: TEXT := NIL;
		      date: RCSDate.T := NIL): RCSDelta.T
  RAISES {RCSError.E} =
  CONST
    ImportDateFudge = 3.5d0;
    (* If the date stamps of revisions 1.1 and 1.1.1.1 differ by
       less than "ImportDateFudge" seconds, we assume the file was
       originally brought into the repository by a "cvs import". *)
  VAR
    tlp: RCSTagList.T;
    revNum: RCSRevNum.T;
    delta, delta1111: RCSDelta.T;
  BEGIN
    IF tag # NIL THEN  (* The caller specified a tag. *)
      tlp := rf.tagList;
      WHILE tlp # NIL DO
	IF Text.Equal(tlp.head.name, tag) THEN EXIT END;
	tlp := tlp.tail;
      END;
      IF tlp = NIL THEN
	Oops(rf, "No such tag " & tag);
      END;
      revNum := tlp.head.revNum;
    ELSE
      revNum := NIL;
    END;

    delta := GetRevDateDelta(rf, revNum, date);
    IF revNum = NIL AND RCSRevNum.Equal(delta.revision, "1.1") THEN
      (* A date-only search has found revision 1.1.  If this file was
         apparently created by a "cvs import", take the appropriate
	 revision from the vendor branch instead. *)
      TRY
	delta1111 := GetDelta(rf, "1.1.1.1");
	IF ABS(RCSDate.ToTime(delta.date) - RCSDate.ToTime(delta1111.date))
	  < ImportDateFudge
	THEN
	  RETURN GetRevDateDelta(rf, "1.1.1", date);
	END;
      EXCEPT RCSError.E => (* Just continue. *) END;
    END;
    RETURN delta;
  END GetTagDelta;

PROCEDURE GetRevDateDelta(rf: T;
                          revNum: RCSRevNum.T := NIL;
		          date: RCSDate.T := NIL): RCSDelta.T
  RAISES {RCSError.E} =
  VAR
    delta: RCSDelta.T;
    isCVSBranch := FALSE;
    isSpecific := FALSE;
    ok: BOOLEAN;
  BEGIN
    IF revNum # NIL THEN  (* The caller specified a revision number. *)
      IF RCSRevNum.NumParts(revNum) MOD 2 = 0 THEN
	(* Specific revision, or special CVS branch. *)
	WITH last = RCSRevNum.Last(revNum), prefix = RCSRevNum.Prefix(revNum) DO
	  IF NOT Text.Equal(RCSRevNum.Last(prefix), "0") THEN
	    (* Specific revision. *)
	    isSpecific := TRUE;
	    delta := GetDelta(rf, revNum);
	  ELSE
	    isCVSBranch := TRUE;
	    (* Convert the CVS branch into an RCS branch, and try
	       to get the tip of that branch.  If the branch doesn't
	       exist, get the branch-point delta instead. *)
	    revNum := RCSRevNum.Cat(RCSRevNum.Prefix(prefix), last);
	    TRY
	      delta := GetBranchTip(rf, revNum);
	    EXCEPT RCSError.E =>
	      delta := GetDelta(rf, RCSRevNum.Prefix(revNum));
	    END;
	  END;
	END;
      ELSE  (* RCS branch. *)
	delta := GetBranchTip(rf, revNum);
      END;
    ELSIF rf.branch # NIL THEN  (* Use the tip of the default branch. *)
      revNum := rf.branch;
      delta := GetBranchTip(rf, revNum);
    ELSIF rf.head # NIL THEN  (* Use the head. *)
      revNum := RCSRevNum.Prefix(rf.head.revision);
      delta := rf.head;
    ELSE
      Oops(rf, "File contains no deltas");
    END;

    (* At this point, "revNum" is guaranteed to be set non-NIL.  For a
       specific revision, it is the revision number of the specific
       delta.  For a branch, or for the default branch, it is the
       revision number of the branch. *)

    IF date # NIL THEN  (* The caller specified a date. *)
      IF isSpecific THEN
	ok := RCSDate.Compare(delta.date, date) <= 0;
      ELSE
	WHILE delta # NIL AND RCSDate.Compare(delta.date, date) > 0 DO
	  delta := RCSDelta.Predecessor(delta);
	END;
	IF delta = NIL THEN
	  ok := FALSE;
	ELSE
	  (* If the delta is on the correct branch, then it is OK. *)
	  IF RCSRevNum.NumParts(revNum) = 1 THEN  (* Must be on the trunk. *)
	    ok := RCSRevNum.IsTrunk(delta.revision);
	  ELSE  (* Must be on the same branch. *)
	    ok := RCSRevNum.Equal(revNum, RCSRevNum.Prefix(delta.revision));
	  END;
	  (* If it's a CVS branch, then it is also OK if the delta is at
	     the branch point. *)
	  IF NOT ok AND isCVSBranch THEN
	    ok := RCSRevNum.Equal(RCSRevNum.Prefix(revNum), delta.revision);
	  END;
	END;
      END;
      IF NOT ok THEN
	Oops(rf, "Non-existent revision/date combination");
      END;
    END;

    RETURN delta;
  END GetRevDateDelta;

PROCEDURE GetBranchTip(rf: T; branch: RCSRevNum.T): RCSDelta.T
  RAISES {RCSError.E} =
  VAR
    delta: RCSDelta.T;
    dlp: RCSDeltaList.T;
  BEGIN
    IF RCSRevNum.NumParts(branch) = 1 THEN  (* Main branch. *)
      delta := rf.head;
      WHILE delta # NIL DO
	IF RCSRevNum.Equal(RCSRevNum.Prefix(delta.revision), branch) THEN
	  EXIT;
	END;
	delta := delta.next;
      END;
      IF delta = NIL THEN
	Oops(rf, "No such branch " & branch);
      END;
    ELSE  (* A branch off of a delta. *)
      delta := GetDelta(rf, RCSRevNum.Prefix(branch));  (* The branch point. *)
      dlp := delta.branches;
      WHILE dlp # NIL DO
	IF RCSRevNum.Equal(RCSRevNum.Prefix(dlp.head.revision), branch) THEN
	  EXIT;
	END;
	dlp := dlp.tail;
      END;
      IF dlp = NIL THEN
	Oops(rf, "No such branch " & branch);
      END;

      (* Now follow the branch to its tip. *)
      delta := dlp.head;
      WHILE delta.next # NIL DO
	delta := delta.next;
      END;
    END;

    RETURN delta;
  END GetBranchTip;

PROCEDURE GetAttr(rf: T): FileAttr.T =
  BEGIN
    RETURN rf.attr;
  END GetAttr;

PROCEDURE GetToken(rf: T) =
  CONST
    WS = SET OF CHAR{' ', '\010', '\t', '\n', '\013', '\f', '\r'};
    Special = SET OF CHAR{'$', ',', '.', ':', ';', '@'};
    Sym = SET OF CHAR{'!' .. '~'} - Special;
    ID = Sym + SET OF CHAR{'.'};
    Digit = SET OF CHAR{'0' .. '9'};
    Num = Digit + SET OF CHAR{'.'};
  VAR
    start: UNTRACED REF CHAR;
    line: CARDINAL;
    ch: CHAR;
  BEGIN
    WHILE rf.ptr < rf.limit AND rf.ptr^ IN WS DO
      IF rf.ptr^ = '\n' THEN INC(rf.line) END;
      INC(rf.ptr);
    END;
    start := rf.ptr;
    line := rf.line;

    IF rf.ptr = rf.limit THEN
      rf.curTok.type := TokType.EOF;
      rf.curTok.keyword := Keyword.None;
      rf.curTok.line := line;
      rf.curTok.ptr := start;
      rf.curTok.len := 0;
      RETURN;
    END;

    IF rf.ptr^ IN ID THEN
      VAR
	type := TokType.Num;
	keyOrd := ORD(Keyword.None);
	ch: CHAR;
      BEGIN
	WHILE rf.ptr < rf.limit DO
	  ch := rf.ptr^;
	  IF NOT ch IN ID THEN EXIT END;
	  IF NOT ch IN Num THEN
	    type := TokType.Id;
	  END;
	  INC(rf.ptr);
	END;
	rf.curTok.ptr := start;
	rf.curTok.len := rf.ptr - start;
	IF type = TokType.Id THEN
	  EVAL keyTab.get(TokText(rf.curTok), keyOrd);
	END;
	rf.curTok.type := type;
	rf.curTok.keyword := VAL(keyOrd, Keyword);
	rf.curTok.line := line;
	RETURN;
      END;
    END;

    CASE rf.ptr^ OF
    | ';' =>
	INC(rf.ptr);
	rf.curTok.type := TokType.Semicolon;
	rf.curTok.keyword := Keyword.None;
	rf.curTok.line := line;
	rf.curTok.ptr := start;
	rf.curTok.len := 1;
	RETURN;
    | ':' =>
	INC(rf.ptr);
	rf.curTok.type := TokType.Colon;
	rf.curTok.keyword := Keyword.None;
	rf.curTok.line := line;
	rf.curTok.ptr := start;
	rf.curTok.len := 1;
	RETURN;
    | '@' =>
	INC(rf.ptr);
	start := rf.ptr;
	LOOP
	  IF rf.ptr = rf.limit THEN  (* Unterminated string. *)
	    rf.curTok.type := TokType.Bad;
	    rf.curTok.keyword := Keyword.None;
	    rf.curTok.line := line;
	    rf.curTok.ptr := start;
	    rf.curTok.len := rf.ptr - start;
	    RETURN;
	  END;
	  ch := rf.ptr^;
	  INC(rf.ptr);
	  IF ch = '@' THEN
	    IF rf.ptr = rf.limit OR rf.ptr^ # '@' THEN EXIT END;
	    INC(rf.ptr);
	  ELSIF ch = '\n' THEN
	    INC(rf.line);
	  END;
	END;
	rf.curTok.type := TokType.String;
	rf.curTok.keyword := Keyword.None;
	rf.curTok.line := line;
	rf.curTok.ptr := start;
	rf.curTok.len := rf.ptr - 1 - start;
	RETURN;
    ELSE
      INC(rf.ptr);
      rf.curTok.type := TokType.Bad;
      rf.curTok.keyword := Keyword.None;
      rf.curTok.line := line;
      rf.curTok.ptr := start;
      rf.curTok.len := 1;
      RETURN;
    END;
  END GetToken;

PROCEDURE Import(p: Pathname.T;
                 revNum: RCSRevNum.T;
		 author: TEXT;
		 state: TEXT;
		 logLines := -1): T
  RAISES {OSError.E} =
  (* Any RCSError.E that gets raised in this procedure really is due to
     an internal error.  We go ahead and let the core dump happen so that
     we can find the bug. *)
  <* FATAL RCSError.E *>
  VAR
    rf: T;
    statbuf: Ustat.struct_stat;
    np: CARDINAL;
    stack: TextSeq.T;
    date: RCSDate.T;
    delta, pred: RCSDelta.T;
    logEdits: TEXT;
  BEGIN
      rf := NEW(T).init();
      rf.buf := UnixMisc.MapFile(p, statbuf);
      rf.attr := FileAttr.FromStat(statbuf);
      rf.len := VAL(statbuf.st_size, INTEGER);
      rf.start := rf.buf;
      rf.limit := rf.start + rf.len;
      rf.ptr := rf.limit;  (* Already at "end of file". *)

      date := RCSDate.FromTime(FileAttr.GetModTime(rf.attr));

      np := RCSRevNum.NumParts(revNum);
      IF np = 0 THEN
	revNum := "1";
	INC(np);
      END;
      IF np MOD 2 = 1 THEN
	revNum := RCSRevNum.Cat(revNum, "1");
	INC(np);
      END;

      stack := NEW(TextSeq.T).init();
      WHILE np > 2 DO
	stack.addhi(revNum);
	revNum := RCSRevNum.Prefix(RCSRevNum.Prefix(revNum));
	DEC(np, 2);
      END;

      pred := NIL;
      delta := AddDelta(rf,
	revNum := revNum,
	diffBase := pred,
	date := date,
	author := author,
	state := state,
	log := RCSString.FromText("Initial revision\n"),
	text := NEW(SimpleString, ptr := rf.start, len := rf.len));
      delta.isPlaceHolder := TRUE;

      WHILE stack.size() > 0 DO
	revNum := stack.remhi();
	pred := delta;
	delta := AddDelta(rf,
	  revNum := revNum,
	  diffBase := pred,
	  date := date,
	  author := author,
	  state := state,
	  log := RCSString.FromText("Initial import.\n"),
	  text := RCSString.FromText(""));
	delta.isPlaceHolder := TRUE;
      END;

      delta.isPlaceHolder := FALSE;  (* The last delta is the real one. *)

      IF logLines >= 0 THEN
	logEdits := MakeLogEdits(rf, logLines);
	IF NOT Text.Empty(logEdits) THEN
	  DeleteDelta(rf, delta);
	  delta := AddDelta(rf,
	    revNum := revNum,
	    diffBase := delta,
	    date := date,
	    author := author,
	    state := state,
	    log := RCSString.FromText("Initial import.\n"),
	    text := RCSString.FromText(logEdits));
	END;
      END;

      RETURN rf;
  END Import;

PROCEDURE Init(rf: T;
               desc: RCSString.T := NIL): T =
  BEGIN
    IF desc = NIL THEN desc := RCSString.FromText("") END;

    rf.attr := NEW(FileAttr.T).init(FileAttr.FileType.File);
    rf.deltaTbl := NEW(SortedRCSDeltaTbl.Default).init();
    rf.desc := desc;
    rf.curTok := NEW(Token);

    RETURN rf;
  END Init;

PROCEDURE MakeLogEdits(rf: T; logLines: CARDINAL): TEXT =
  TYPE
    State = {
      Idle, NeedL, Needo, Needg, NeedColon,
      FindDollar, FindNewline, Voila, Ignore
    };
  VAR
    ptr: UNTRACED REF CHAR := rf.start;
    limit: UNTRACED REF CHAR := rf.start + rf.len;
    lineNum := 1;
    state := State.Idle;
    edits := "";
    ch: CHAR;
    ignoreCount: CARDINAL;
  BEGIN
    WHILE ptr < limit DO
      ch := ptr^;
      IF state # State.Idle THEN
	CASE state OF
	| State.Idle =>
	    <* ASSERT FALSE *>
	| State.NeedL =>
	    IF ch = 'L' THEN
	      state := State.Needo;
	    ELSE
	      state := State.Idle;
	    END;
	| State.Needo =>
	    IF ch = 'o' THEN
	      state := State.Needg;
	    ELSE
	      state := State.Idle;
	    END;
	| State.Needg =>
	    IF ch = 'g' THEN
	      state := State.NeedColon;
	    ELSE
	      state := State.Idle;
	    END;
	| State.NeedColon =>
	    IF ch = ':' THEN
	      state := State.FindDollar;
	    ELSE
	      state := State.Idle;
	    END;
	| State.FindDollar =>
	    IF ch = '$' THEN
	      state := State.FindNewline;
	    ELSIF ch = '\n' THEN
	      state := State.Idle;
	    END;
	| State.FindNewline =>
	    IF ch = '\n' THEN
	      state := State.Voila;
	    END;
	| State.Voila =>
	    edits := edits &
	      "d" & Fmt.Int(lineNum) & " " & Fmt.Int(logLines+2) & "\n";
	    ignoreCount := logLines;
	    state := State.Ignore;
	| State.Ignore =>
	    IF ch = '\n' THEN
	      DEC(ignoreCount);
	      IF ignoreCount = 0 THEN
		state := State.Idle;
	      END;
	    END;
	END;
      ELSIF ch = '$' THEN
	state := State.NeedL;
      END;

      IF ch = '\n' THEN INC(lineNum) END;
      INC(ptr);
    END;
    RETURN edits;
  END MakeLogEdits;

PROCEDURE OpenReadonly(p: Pathname.T): T
  RAISES {OSError.E, RCSError.E} =
  VAR
    rf: T;
    statbuf: Ustat.struct_stat;
  BEGIN
    rf := NEW(T).init();
    rf.buf := UnixMisc.MapFile(p, statbuf);
    rf.attr := FileAttr.FromStat(statbuf);
    rf.len := VAL(statbuf.st_size, INTEGER);
    rf.start := rf.buf;
    rf.ptr := rf.start;
    rf.limit := rf.start + rf.len;

    TRY
      ParseAdmin(rf);
      ParseTree(rf);
      ParseDesc(rf);
      RETURN rf;
    EXCEPT RCSError.E(msg) =>
      TRY Close(rf) EXCEPT OSError.E => (* Ignore *) END;
      RAISE RCSError.E(msg);
    END;
  END OpenReadonly;

PROCEDURE ParseAdmin(rf: T) RAISES {RCSError.E} =
  VAR
    lastAccess: RCSAccessList.T := NIL;
    lastTag: RCSTagList.T := NIL;
  BEGIN
    (* head {num}; *)
    EatKeyword(rf, Keyword.Head);
    (* Figure out whether this RCS file is in the format produced by CVS
       for an initial import.  CVS doesn't bother to generate the same
       whitespace as RCS would have, sigh. *)
    IF rf.ptr < rf.limit AND rf.ptr^ = ' ' THEN
      rf.options := rf.options + Options{Option.CVSInitialImport};
    ELSE
      rf.options := rf.options - Options{Option.CVSInitialImport};
    END;
    IF HaveType(rf, TokType.Num) THEN
      rf.head := EnterDelta(rf, TokText(CurTok(rf)));
      EatTok(rf);
    END;
    EatType(rf, TokType.Semicolon);

    (* {branch {num};} *)
    IF HaveKeyword(rf, Keyword.Branch) THEN
      EatTok(rf);
      IF HaveType(rf, TokType.Num) THEN
	rf.branch := TokText(CurTok(rf));
	EatTok(rf);
      END;
      EatType(rf, TokType.Semicolon);
    END;

    (* access {id}*; *)
    EatKeyword(rf, Keyword.Access);
    <* ASSERT rf.accessList = NIL *>
    WHILE HaveType(rf, TokType.Id) DO
      WITH access = NEW(RCSAccess.T) DO
	access.name := TokText(CurTok(rf));
	EatTok(rf);
	WITH l = RCSAccessList.List1(access) DO
	  IF lastAccess = NIL THEN
	    rf.accessList := l;
	  ELSE
	    lastAccess.tail := l;
	  END;
	  lastAccess := l;
	END;
      END;
    END;
    EatType(rf, TokType.Semicolon);

    (* symbols {sym:num}*; *)
    EatKeyword(rf, Keyword.Symbols);
    <* ASSERT rf.tagList = NIL *>
    WHILE HaveType(rf, TokType.Id) DO
      WITH tag = NEW(RCSTag.T) DO
	tag.name := TokText(CurTok(rf));
	EatTok(rf);
	EatType(rf, TokType.Colon);
	NeedType(rf, TokType.Num);
	tag.revNum := TokText(CurTok(rf));
	EatTok(rf);
	WITH l = RCSTagList.List1(tag) DO
	  IF lastTag = NIL THEN
	    rf.tagList := l;
	  ELSE
	    lastTag.tail := l;
	  END;
	  lastTag := l;
	END;
      END;
    END;
    EatType(rf, TokType.Semicolon);

    (* locks {id:num}*; {strict;} *)
    EatKeyword(rf, Keyword.Locks);
    WHILE HaveType(rf, TokType.Id) DO
      EatTok(rf);  (* FIXME *)
      EatType(rf, TokType.Colon);
      EatType(rf, TokType.Num);
    END;
    EatType(rf, TokType.Semicolon);
    IF HaveKeyword(rf, Keyword.Strict) THEN
      rf.strictLocking := TRUE;
      EatTok(rf);
      EatType(rf, TokType.Semicolon);
    ELSE
      rf.strictLocking := FALSE;
    END;

    (* {comment {string};} *)
    IF HaveKeyword(rf, Keyword.Comment) THEN
      EatTok(rf);
      IF HaveType(rf, TokType.String) THEN
	rf.comment := TokText(CurTok(rf));
	EatTok(rf);
      END;
      EatType(rf, TokType.Semicolon);
    END;

    (* {expand {string};} *)
    IF HaveKeyword(rf, Keyword.Expand) THEN
      EatTok(rf);
      IF HaveType(rf, TokType.String) THEN
	rf.expand := RCSKeyword.DecodeExpand(TokText(CurTok(rf)));
	EatTok(rf);
      END;
      EatType(rf, TokType.Semicolon);
    END;

    (* {newphrase}* *)
    rf.newPhrases := ParseNewPhrases(rf);
  END ParseAdmin;

PROCEDURE ParseTree(rf: T) RAISES {RCSError.E} =
  VAR
    delta: RCSDelta.T;
    ok: BOOLEAN;
  BEGIN
    WHILE HaveType(rf, TokType.Num) DO
      delta := EnterDelta(rf, TokText(CurTok(rf)));
      EatTok(rf);

      (* date num; *)
      EatKeyword(rf, Keyword.Date);
      NeedType(rf, TokType.Num);
      delta.date := TokText(CurTok(rf));
      EatTok(rf);
      EatType(rf, TokType.Semicolon);

      (* author id; *)
      EatKeyword(rf, Keyword.Author);
      NeedType(rf, TokType.Id);
      delta.author := TokText(CurTok(rf));
      EatTok(rf);
      EatType(rf, TokType.Semicolon);

      (* state {id}; *)
      EatKeyword(rf, Keyword.State);
      IF HaveType(rf, TokType.Id) THEN
	delta.state := TokText(CurTok(rf));
	EatTok(rf);
      END;
      EatType(rf, TokType.Semicolon);

      (* branches {num}*; *)
      EatKeyword(rf, Keyword.Branches);
      WHILE HaveType(rf, TokType.Num) DO
	WITH br = TokText(CurTok(rf)) DO
	  IF NOT RCSRevNum.Equal(RCSRevNum.Prefix(RCSRevNum.Prefix(br)),
	    delta.revision)
	  THEN
	    RAISE RCSError.E("Invalid branch " & br & " for delta "
	      & delta.revision);
	  END;
	  RCSDeltaClass.AddBranch(delta, EnterDelta(rf, br), delta);
	END;
	EatTok(rf);
      END;
      EatType(rf, TokType.Semicolon);

      (* next {num}; *)
      EatKeyword(rf, Keyword.Next);
      IF HaveType(rf, TokType.Num) THEN
	WITH next = EnterDelta(rf, TokText(CurTok(rf))) DO
	  IF RCSRevNum.IsTrunk(delta.revision) THEN
	    ok := RCSRevNum.IsTrunk(next.revision)
	      AND RCSRevNum.Compare(next.revision, delta.revision) < 0;
	  ELSE
	    ok := RCSRevNum.Compare(next.revision, delta.revision) > 0
	      AND RCSRevNum.Equal(RCSRevNum.Prefix(next.revision),
		RCSRevNum.Prefix(delta.revision));
	  END;
	  IF NOT ok THEN
	    RAISE RCSError.E("Invalid next delta " & next.revision
	      & " for delta " & delta.revision);
	  END;
	  delta.next := next;
	  next.prev := delta;
	  next.diffBase := delta;
	END;
	EatTok(rf);
      ELSIF RCSRevNum.IsTrunk(delta.revision) THEN
	rf.tail := delta;
      END;
      EatType(rf, TokType.Semicolon);

      (* {newphrase}* *)
      delta.treePhrases := ParseNewPhrases(rf);
    END;
  END ParseTree;

PROCEDURE ParseDelta(rf: T; delta: RCSDelta.T)
  RAISES {RCSError.E} =
  BEGIN
    WHILE NOT delta.isParsed DO
      ParseOneDeltaText(rf);
    END;
  END ParseDelta;

PROCEDURE ParseOneDeltaText(rf: T) RAISES {RCSError.E} =
  VAR
    revision: RCSRevNum.T;
    delta: RCSDelta.T;
  BEGIN
    (* num *)
    NeedType(rf, TokType.Num);
    revision := TokText(CurTok(rf));
    EatTok(rf);
    IF NOT rf.deltaTbl.get(revision, delta) THEN
      Oops(rf, "Missing revision " & revision);
    END;

    (* log string *)
    EatKeyword(rf, Keyword.Log);
    NeedType(rf, TokType.String);
    WITH tok = CurTok(rf) DO
      delta.log := NEW(QuotedString, ptr := tok.ptr, len := tok.len);
    END;
    EatTok(rf);

    (* {newphrase}* *)
    delta.textPhrases := ParseNewPhrases(rf);

    (* text string *)
    EatKeyword(rf, Keyword.Text);
    NeedType(rf, TokType.String);
    WITH tok = CurTok(rf) DO
      delta.text := NEW(QuotedString, ptr := tok.ptr, len := tok.len);
    END;
    EatTok(rf);
    delta.isParsed := TRUE;
  END ParseOneDeltaText;

PROCEDURE ParseDesc(rf: T) RAISES {RCSError.E} =
  VAR
    descEndingLine: CARDINAL;
  BEGIN
    (* desc string *)
    EatKeyword(rf, Keyword.Desc);
    NeedType(rf, TokType.String);
    WITH tok = CurTok(rf) DO
      rf.desc := NEW(QuotedString, ptr := tok.ptr, len := tok.len);
    END;
    EatTok(rf);

    (* Peek ahead at the next token, and figure out how many blank lines
       are between the end of the desc string and the start of the next
       token.  This varies depending on whether the initial checkin was
       done via RCS, or directly by CVS import.  We bother to check this
       so that we can try to generate an exact replica of the original
       file when we write it out. *)
    descEndingLine := rf.line;
    EVAL CurTok(rf);
    IF rf.line - descEndingLine - 1 > 2 THEN
      rf.options := rf.options + Options{Option.ExtraLineAfterDesc};
    ELSE
      rf.options := rf.options - Options{Option.ExtraLineAfterDesc};
    END;
  END ParseDesc;

PROCEDURE ParseNewPhrases(rf: T): RCSPhrases.T
  RAISES {RCSError.E} =
  VAR
    phrases: RCSPhrases.T := NIL;
    phrase: RCSPhrase.T;
  BEGIN
    IF HaveKeyword(rf, Keyword.None) THEN
      phrases := RCSPhrases.New();
      REPEAT
	phrase := RCSPhrase.New(TokText(rf.curTok));
	EatTok(rf);
	WHILE HaveType(rf, TokType.Id)
	OR HaveType(rf, TokType.Num)
	OR HaveType(rf, TokType.String)
	OR HaveType(rf, TokType.Colon)
	DO
	  RCSPhrase.Append(phrase, TokText(rf.curTok),
	    HaveType(rf, TokType.String));
	  EatTok(rf);
	END;
	EatType(rf, TokType.Semicolon);
	RCSPhrases.Append(phrases, phrase);
      UNTIL NOT HaveKeyword(rf, Keyword.None);
    END;
    RETURN phrases;
  END ParseNewPhrases;

PROCEDURE EnterDelta(rf: T; revision: RCSRevNum.T): RCSDelta.T =
  VAR
    delta: RCSDelta.T;
  BEGIN
    IF NOT rf.deltaTbl.get(revision, delta) THEN
      delta := NEW(RCSDelta.T, rcsFile := rf, revision := revision);
      EVAL rf.deltaTbl.put(revision, delta);
    END;
    RETURN delta;
  END EnterDelta;
*************************************************************************** Modifying already-parsed RCS files. ***************************************************************************

PROCEDURE AddDelta(rf: T;
                   revNum: RCSRevNum.T;
		   diffBase: RCSDelta.T;
                   date: TEXT;
		   author: TEXT;
		   state: TEXT;
		   log: RCSString.T;
		   text: RCSString.T;
		   treePhrases: RCSPhrases.T := NIL;
		   textPhrases: RCSPhrases.T := NIL): RCSDelta.T
  RAISES {RCSError.E} =
  VAR
    delta: RCSDelta.T;
    oldDelta: RCSDelta.T;
    next: RCSDelta.T;
    prev: RCSDelta.T;
    bp: RCSDelta.T;
    branch: RCSDelta.T;
    branchRevNum: RCSRevNum.T;
    bpRevNum: RCSRevNum.T;
  BEGIN
    WITH n = RCSRevNum.NumParts(revNum) DO
      IF n < 2 OR n MOD 2 # 0 THEN
	Oops(rf, "Attempt to add invalid revision number " & revNum);
      END;
    END;
    delta := NEW(RCSDelta.T,
      rcsFile := rf,
      revision := revNum,
      date := date,
      author := author,
      state := state,
      log := log,
      text := text,
      treePhrases := treePhrases,
      textPhrases := textPhrases,
      diffBase := diffBase,
      isParsed := TRUE);

    IF rf.deltaTbl.get(revNum, oldDelta) THEN  (* Delta already exists. *)
      IF NOT oldDelta.isPlaceHolder THEN
	Oops(rf, "Attempt to add existing delta " & revNum);
      END;
      prev := oldDelta.prev;
      next := oldDelta.next;
      delta.branches := oldDelta.branches;
      oldDelta.branches := NIL;
      DeleteDelta(rf, oldDelta);
    ELSE
      oldDelta := NIL;
      prev := NIL;
      next := NIL;
    END;

    IF RCSRevNum.IsTrunk(revNum) THEN
      IF oldDelta = NIL THEN  (* Find the insertion point. *)
	prev := NIL;
	next := rf.head;
	WHILE next # NIL AND RCSRevNum.Compare(next.revision, revNum) >= 0 DO
	  prev := next;
	  next := next.next;
	END;
      END;
      delta.prev := prev;
      delta.next := next;
      IF delta.prev # NIL THEN
	delta.prev.next := delta;
      ELSE
	rf.head := delta;
      END;
      IF delta.next # NIL THEN
	delta.next.prev := delta;
      ELSE
	rf.tail := delta;
      END;
    ELSE
      branchRevNum := RCSRevNum.Prefix(revNum);
      bpRevNum := RCSRevNum.Prefix(branchRevNum);
      IF NOT rf.deltaTbl.get(bpRevNum, bp) THEN
	Oops(rf, "No branch point for adding delta " & revNum);
      END;

      TRY
	branch := RCSDelta.GetBranch(bp, branchRevNum);
      EXCEPT RCSError.E =>
	branch := NIL;
      END;

      IF branch = NIL THEN
	RCSDeltaClass.AddBranch(bp, delta, diffBase);
      ELSE
	IF oldDelta = NIL THEN  (* Find the insertion point. *)
	  prev := bp;
	  next := branch;
	  WHILE next # NIL AND RCSRevNum.Compare(next.revision, revNum) <= 0 DO
	    prev := next;
	    next := next.next;
	  END;
	END;
	delta.prev := prev;
	delta.next := next;
	IF delta.prev # bp THEN
	  delta.prev.next := delta;
	ELSE
	  RCSDeltaClass.ChangeBranch(bp, delta.next, delta);
	END;
	IF delta.next # NIL THEN delta.next.prev := delta END;
      END;
    END;
    EVAL rf.deltaTbl.put(revNum, delta);
    RETURN delta;
  END AddDelta;

PROCEDURE AddTag(rf: T; name: TEXT; revNum: RCSRevNum.T): RCSTag.T
  RAISES {RCSError.E} =
  VAR
    tag := NEW(RCSTag.T, name := name, revNum := revNum);
    rem := RCSRevNum.NumParts(revNum) MOD 2;
    p := rf.tagList;
  BEGIN
    WHILE p # NIL DO
      IF RCSTag.Equal(p.head, tag) AND
	RCSRevNum.NumParts(p.head.revNum) MOD 2 = rem THEN
	Oops(rf, "Attempt to add existing tag " & name);
      END;
      p := p.tail;
    END;
    rf.tagList := RCSTagList.Cons(tag, rf.tagList);
    RETURN tag;
  END AddTag;

PROCEDURE DeleteDelta(rf: T; delta: RCSDelta.T)
  RAISES {RCSError.E} =
  BEGIN
    IF delta.branches # NIL THEN
      Oops(rf, "Attempt to delete delta (" & delta.revision
	& ") with branches");
    END;

    (* Parse the delta, if it has not already been parsed.  We may
       need it to be parsed later, e.g., if it is used as a diff
       base, or if we need to parse through it to get to a delta
       farther down in the file.  Once we have removed it from the
       delta table, it won't be possible to parse it any more, so
       to be safe, we have to do it now. *)
    ParseDelta(rf, delta);

    IF RCSRevNum.IsTrunk(delta.revision) THEN
      IF delta.prev # NIL THEN
	delta.prev.next := delta.next;
      ELSE
	rf.head := delta.next;
      END;
      IF delta.next # NIL THEN
	delta.next.prev := delta.prev;
      ELSE
	rf.tail := delta.prev;
      END;
    ELSE
      IF delta.prev.next # delta THEN  (* First delta on its branch. *)
	IF delta.next = NIL THEN  (* Only delta on its branch. *)
	  RCSDeltaClass.DeleteBranch(delta.prev, delta);
	ELSE
	  RCSDeltaClass.ChangeBranch(delta.prev, delta, delta.next);
	  delta.next.prev := delta.prev;
	END;
      ELSE
	delta.prev.next := delta.next;
	IF delta.next # NIL THEN delta.next.prev := delta.prev END;
      END;
    END;
    delta.next := NIL;
    delta.prev := NIL;
    EVAL rf.deltaTbl.delete(delta.revision, delta);
  END DeleteDelta;

PROCEDURE DeleteTag(rf: T; name: TEXT; revNum: RCSRevNum.T)
  RAISES {RCSError.E} =
  VAR
    p := rf.tagList;
    last: RCSTagList.T := NIL;
  BEGIN
    WHILE p # NIL DO
      IF Text.Equal(p.head.name, name) AND
	RCSRevNum.Equal(p.head.revNum, revNum) THEN
	IF last = NIL THEN
	  rf.tagList := p.tail;
	ELSE
	  last.tail := p.tail;
	END;
	RETURN;
      END;
      last := p;
      p := p.tail;
    END;
    Oops(rf, "No such tag " & name & ":" & revNum);
  END DeleteTag;
*************************************************************************** Writing to a file ***************************************************************************

PROCEDURE ToWr(rf: T; wr: Wr.T)
  RAISES {RCSError.E, Thread.Alerted, Wr.Failure} =
  BEGIN
    PutAdmin(rf, wr);
    PutDeltas(rf, wr);
    PutDesc(rf, wr);
    PutDeltaTexts(rf, wr);
  END ToWr;

PROCEDURE PutAdmin(rf: T; wr: Wr.T)
  RAISES {Thread.Alerted, Wr.Failure} =
  VAR
    accessIter: AccessIterator;
    access: RCSAccess.T;
    tagIter: TagIterator;
    tag: RCSTag.T;
    headWS: TEXT;
    branchWS: TEXT;
    accessWS: TEXT;
    accessSep: TEXT;
    symbolsWS: TEXT;
    symbolSep: TEXT;
    locksWS: TEXT;
    commentWS: TEXT;
    expandWS: TEXT;
  BEGIN
    IF Option.CVSInitialImport IN rf.options THEN
      headWS := "     ";
      branchWS := "   ";
      accessWS := "   ";
      accessSep := "   ";
      symbolsWS := " ";
      symbolSep := " ";
      locksWS := "    ";
      commentWS := "  ";
      expandWS := "   ";
    ELSE
      headWS := "\t";
      branchWS := "\t";
      accessWS := "";
      accessSep := "\n\t";
      symbolsWS := "";
      symbolSep := "\n\t";
      locksWS := "";
      commentWS := "\t";
      expandWS := "\t";
    END;

    Wr.PutText(wr, "head" & headWS);
    IF rf.head # NIL THEN
      Wr.PutText(wr, rf.head.revision);
    END;
    Wr.PutText(wr, ";\n");
    IF rf.branch # NIL THEN
      Wr.PutText(wr, "branch" & branchWS & rf.branch & ";\n");
    END;

    Wr.PutText(wr, "access" & accessWS);
    accessIter := IterateAccess(rf);
    WHILE accessIter.next(access) DO
      Wr.PutText(wr, accessSep & access.name);
    END;
    Wr.PutText(wr, ";\n");

    Wr.PutText(wr, "symbols" & symbolsWS);
    tagIter := IterateTags(rf);
    WHILE tagIter.next(tag) DO
      Wr.PutText(wr, symbolSep & tag.name & ":" & tag.revNum);
    END;
    Wr.PutText(wr, ";\n");

    Wr.PutText(wr, "locks" & locksWS & ";");  (* FIXME *)
    IF rf.strictLocking THEN
      Wr.PutText(wr, " strict;");
    END;
    Wr.PutChar(wr, '\n');
    IF rf.comment # NIL THEN
      Wr.PutText(wr, "comment" & commentWS & "@");
      PutEscaped(wr, rf.comment);
      Wr.PutText(wr, "@;\n");
    END;
    IF rf.expand # RCSKeyword.ExpandMode.Default THEN
      Wr.PutText(wr, "expand" & expandWS & "@");
      PutEscaped(wr, RCSKeyword.EncodeExpand(rf.expand));
      Wr.PutText(wr, "@;\n");
    END;
    PutPhrases(wr, rf.newPhrases);
    Wr.PutChar(wr, '\n');
  END PutAdmin;

PROCEDURE PutDeltas(rf: T; wr: Wr.T)
  RAISES {Thread.Alerted, Wr.Failure} =
  VAR
    stack: RCSDeltaList.T := NIL;
    delta: RCSDelta.T;
    iter: RCSDelta.Iterator;
    branch: RCSDelta.T;
    dateWS: TEXT;
    authorWS: TEXT;
    stateWS: TEXT;
    branchesWS: TEXT;
    branchesSep: TEXT;
    nextWS: TEXT;
  BEGIN
    IF Option.CVSInitialImport IN rf.options THEN
      dateWS := "     ";
      authorWS := "  ";
      stateWS := "  ";
      branchesWS := " ";
      branchesSep := "";
      nextWS := "     ";
    ELSE
      dateWS := "\t";
      authorWS := "\t";
      stateWS := "\t";
      branchesWS := "";
      branchesSep := "\n\t";
      nextWS := "\t";
    END;

    (* Emit the deltas in preorder.  We use the stack algorithm rather
       than recursion, because the recursion can become quite deep.  Since
       we are running in a thread, we don't have much stack space to waste. *)
    IF rf.head # NIL THEN
      stack := RCSDeltaList.Cons(rf.head, stack);
    END;
    WHILE stack # NIL DO
      (* Pop top delta from stack. *)
      delta := stack.head;
      stack := stack.tail;

      (* Emit the delta. *)
      Wr.PutText(wr, "\n" & delta.revision & "\n");
      Wr.PutText(wr, "date" & dateWS & delta.date & ";" &
	authorWS & "author " & delta.author & ";" &
	stateWS & "state ");
      IF delta.state # NIL THEN Wr.PutText(wr, delta.state) END;
      Wr.PutText(wr, ";\n");

      Wr.PutText(wr, "branches" & branchesWS);
      iter := RCSDelta.IterateBranches(delta);
      WHILE iter.next(branch) DO
	Wr.PutText(wr, branchesSep & branch.revision);
      END;
      Wr.PutText(wr, ";\n");

      Wr.PutText(wr, "next" & nextWS);
      IF delta.next # NIL THEN Wr.PutText(wr, delta.next.revision) END;
      Wr.PutText(wr, ";\n");

      PutPhrases(wr, delta.treePhrases);

      (* Push children in reverse order. *)
      iter := RCSDelta.IterateBranchesReversed(delta);
      WHILE iter.next(branch) DO
	stack := RCSDeltaList.Cons(branch, stack);
      END;
      IF delta.next # NIL THEN
	stack := RCSDeltaList.Cons(delta.next, stack);
      END;
    END;
  END PutDeltas;

PROCEDURE PutDeltaTexts(rf: T; wr: Wr.T)
  RAISES {RCSError.E, Thread.Alerted, Wr.Failure} =
  VAR
    delta: RCSDelta.T;
    stack: RCSDeltaList.T := NIL;
    children: RCSDeltaList.T := NIL;
    iter: RCSDelta.Iterator;
    branch: RCSDelta.T;
  BEGIN
    (* Here again, we use pre-order to output the delta texts.  But when
       a node has multiple children (i.e., there are branches hanging
       off of it), we have to be careful about their relative order.  We
       want the newest children to come first, so we have to push them
       on the stack oldest-first.

       Again here we use a stack algorithm rather than recursion, to
       guard against possible thread stack overflow. *)
    IF rf.head # NIL THEN
      stack := RCSDeltaList.Cons(rf.head, stack);
    END;
    WHILE stack # NIL DO
      (* Pop top delta from stack. *)
      delta := stack.head;
      stack := stack.tail;

      (* Emit the delta text. *)
      Wr.PutText(wr, "\n\n" & delta.revision & "\nlog\n");
      PutString(wr, RCSDelta.GetLog(delta).iterate());
      PutPhrases(wr, delta.textPhrases);
      Wr.PutText(wr, "text\n");
      PutString(wr, RCSDelta.GetText(delta, delta.prev));

      (* Push the children oldest-first.  There is a wrinkle here.
	 We have encountered strange RCS files in which there is
	 a revision 1.1 whose date says it is newer than revision
	 3.0.  This "cannot" have happened, since revision 3.0 is
	 derived from 1.1.  These RCS files were probably created
	 by some sort of hackery, but we would nevertheless like
	 to handle them properly.  To do that, we maintain that
	 any node on the main branch is by definition older than
	 its "prev" node, which is in turn older than any other
	 children (branches). *)

      IF delta.next # NIL THEN
	IF RCSRevNum.IsTrunk(delta.revision) THEN  (* Oldest by definition. *)
	  stack := RCSDeltaList.Cons(delta.next, stack);
	ELSE  (* Handle it like the other children. *)
	  children := RCSDeltaList.Cons(delta.next, children);
	END;
      END;
      iter := RCSDelta.IterateBranches(delta);
      WHILE iter.next(branch) DO
	children := RCSDeltaList.Cons(branch, children);
      END;
      children := RCSDeltaListSort.SortD(children, CompByDate);
      WHILE children # NIL DO
	stack := RCSDeltaList.Cons(children.head, stack);
	children := children.tail;
      END;
    END;
  END PutDeltaTexts;

PROCEDURE PutDesc(rf: T; wr: Wr.T)
  RAISES {Thread.Alerted, Wr.Failure} =
  BEGIN
    Wr.PutText(wr, "\n\ndesc\n");
    PutString(wr, rf.desc.iterate());
    IF Option.ExtraLineAfterDesc IN rf.options THEN
      Wr.PutChar(wr, '\n');
    END;
  END PutDesc;

PROCEDURE PutEscaped(wr: Wr.T; t: TEXT)
  RAISES {Thread.Alerted, Wr.Failure} =
  VAR
    atPos := Text.FindChar(t, '@');
    start: CARDINAL;
  BEGIN
    IF atPos = -1 THEN  (* The usual case, no '@' characters. *)
      Wr.PutText(wr, t);
    ELSE  (* There are some '@' characters that we have to double. *)
      start := 0;
      REPEAT
	Wr.PutText(wr, Text.Sub(t, start, atPos + 1 - start));  (* Thru '@' *)
	start := atPos;  (* Will get the '@' again. *)
	atPos := Text.FindChar(t, '@', atPos + 1);
      UNTIL atPos = -1;
      Wr.PutText(wr, Text.Sub(t, start));
    END;
  END PutEscaped;

PROCEDURE PutPhrase(wr: Wr.T; phrase: RCSPhrase.T)
  RAISES {Thread.Alerted, Wr.Failure} =
  VAR
    iter := RCSPhrase.IterateWords(phrase);
    word: TEXT;
    isString: BOOLEAN;
  BEGIN
    Wr.PutText(wr, RCSPhrase.GetKey(phrase));
    IF iter.next(word, isString) THEN
      LOOP
	Wr.PutChar(wr, '\t');
	IF isString THEN
	  Wr.PutChar(wr, '@'); PutEscaped(wr, word); Wr.PutChar(wr, '@');
	ELSE
	  Wr.PutText(wr, word);
	END;
	IF NOT iter.next(word, isString) THEN EXIT END;
	IF NOT isString AND Text.Equal(word, ":") THEN
	  (* Collapse the common form "word:word" onto a single line. *)
	  Wr.PutChar(wr, ':');
	  IF NOT iter.next(word, isString) THEN EXIT END;
	  IF isString THEN
	    Wr.PutChar(wr, '@'); PutEscaped(wr, word); Wr.PutChar(wr, '@');
	  ELSE
	    Wr.PutText(wr, word);
	  END;
	  IF NOT iter.next(word, isString) THEN EXIT END;
	END;
	Wr.PutText(wr, "\n");
      END;
    END;
    Wr.PutText(wr, ";\n");
  END PutPhrase;

PROCEDURE PutPhrases(wr: Wr.T; phrases: RCSPhrases.T)
  RAISES {Thread.Alerted, Wr.Failure} =
  VAR
    iter: RCSPhrases.Iterator;
    phrase: RCSPhrase.T;
  BEGIN
    IF phrases # NIL THEN
      iter := RCSPhrases.Iterate(phrases);
      WHILE iter.next(phrase) DO
	PutPhrase(wr, phrase);
      END;
    END;
  END PutPhrases;

PROCEDURE PutString(wr: Wr.T; iter: RCSString.Iterator)
  RAISES {Thread.Alerted, Wr.Failure} =
  VAR
    line: RCSString.T;
  BEGIN
    Wr.PutChar(wr, '@');
    WHILE iter.next(line) DO
      PutEscaped(wr, line.toText());
    END;
    Wr.PutText(wr, "@\n");
  END PutString;

PROCEDURE CompByDate(d1, d2: RCSDelta.T): [-1..1] =
  VAR
    c: [-1..1];
  BEGIN
    c := RCSDate.Compare(d1.date, d2.date);

    (* It has occurred that revisions 1.1 and 1.1.1.1 had exactly the
       same date, and 1.1.1.1 happened to come out first from a sort.
       We rely on branches coming out before their respective branch
       points.  To prevent problems, we break date ties by comparing
       revision numbers. *)
    IF c = 0 THEN
      c := RCSRevNum.Compare(d1.revision, d2.revision);
    END;
    RETURN c;
  END CompByDate;
*************************************************************************** NewPhrases support ***************************************************************************

PROCEDURE IteratePhrases(rf: T): RCSPhrases.Iterator =
  BEGIN
    RETURN RCSPhrases.Iterate(rf.newPhrases);
  END IteratePhrases;

PROCEDURE AddPhrase(rf: T; phrase: RCSPhrase.T) =
  BEGIN
    IF rf.newPhrases = NIL THEN
      rf.newPhrases := RCSPhrases.New();
    END;
    RCSPhrases.Append(rf.newPhrases, phrase);
  END AddPhrase;

PROCEDURE DeletePhrases(rf: T) =
  BEGIN
    rf.newPhrases := NIL;
  END DeletePhrases;
*************************************************************************** Iteration support ***************************************************************************

TYPE
  AccessIteratorImpl = AccessIterator OBJECT
    cur: RCSAccessList.T;
  OVERRIDES
    next := NextAccess;
  END;

  TagIteratorImpl = TagIterator OBJECT
    cur: RCSTagList.T;
  OVERRIDES
    next := NextTag;
  END;

PROCEDURE IterateByNumber(rf: T; up: BOOLEAN := TRUE): RCSDeltaTbl.Iterator =
  BEGIN
    RETURN rf.deltaTbl.iterateOrdered(up);
  END IterateByNumber;

PROCEDURE IterateAccess(rf: T): AccessIterator =
  BEGIN
    RETURN NEW(AccessIteratorImpl, cur := rf.accessList);
  END IterateAccess;

PROCEDURE IterateTags(rf: T): TagIterator =
  BEGIN
    RETURN NEW(TagIteratorImpl, cur := rf.tagList);
  END IterateTags;

PROCEDURE IterateTagsByName(rf: T): TagIterator =
  BEGIN
    RETURN NEW(TagIteratorImpl, cur := RCSTagListSort.Sort(rf.tagList));
  END IterateTagsByName;

PROCEDURE NextAccess(iter: AccessIteratorImpl; VAR access: RCSAccess.T): BOOLEAN =
  BEGIN
    IF iter.cur = NIL THEN RETURN FALSE END;
    access := iter.cur.head;
    iter.cur := iter.cur.tail;
    RETURN TRUE;
  END NextAccess;

PROCEDURE NextTag(iter: TagIteratorImpl; VAR tag: RCSTag.T): BOOLEAN =
  BEGIN
    IF iter.cur = NIL THEN RETURN FALSE END;
    tag := iter.cur.head;
    iter.cur := iter.cur.tail;
    RETURN TRUE;
  END NextTag;
*************************************************************************** Options support ***************************************************************************

PROCEDURE EncodeOptions(options: Options): TEXT =
  VAR
    flags: Word.T := 0;
  BEGIN
    FOR o := FIRST(Option) TO LAST(Option) DO
      IF o IN options THEN
	flags := Word.Or(flags, Word.LeftShift(1, ORD(o)));
      END;
    END;
    RETURN Fmt.Unsigned(flags, 10);
  END EncodeOptions;

PROCEDURE DecodeOptions(text: TEXT): Options
  RAISES {RCSError.E} =
  VAR
    options := Options{};
  BEGIN
    TRY
      WITH flags = TokScan.AtoI(text) DO
	FOR o := FIRST(Option) TO LAST(Option) DO
	  IF Word.And(flags, Word.LeftShift(1, ORD(o))) # 0 THEN
	    options := options + Options{o};
	  END;
	END;
      END;
      RETURN options;
    EXCEPT TokScan.Error =>
      RAISE RCSError.E("Invalid RCSFile option encoding");
    END;
  END DecodeOptions;
*************************************************************************** Low level parsing routines ***************************************************************************

PROCEDURE EatTok(rf: T) =
  BEGIN
    EVAL CurTok(rf);
    rf.curTok.type := TokType.None;
  END EatTok;

PROCEDURE EatType(rf: T; type: TokType) RAISES {RCSError.E} =
  BEGIN
    NeedType(rf, type);
    EatTok(rf);
  END EatType;

PROCEDURE EatKeyword(rf: T; key: Keyword) RAISES {RCSError.E} =
  BEGIN
    NeedKeyword(rf, key);
    EatTok(rf);
  END EatKeyword;

PROCEDURE NeedType(rf: T; type: TokType) RAISES {RCSError.E} =
  BEGIN
    IF NOT HaveType(rf, type) THEN
      Oops(rf, "\"" & TokTypeName(type) & "\" expected");
    END;
  END NeedType;

PROCEDURE NeedKeyword(rf: T; key: Keyword) RAISES {RCSError.E} =
  BEGIN
    IF NOT HaveKeyword(rf, key) THEN
      Oops(rf, "\"" & KeywordName(key) & "\" expected");
    END;
  END NeedKeyword;

PROCEDURE HaveType(rf: T; type: TokType): BOOLEAN =
  BEGIN
    RETURN CurTok(rf).type = type;
  END HaveType;

PROCEDURE HaveKeyword(rf: T; key: Keyword): BOOLEAN =
  BEGIN
    RETURN HaveType(rf, TokType.Id) AND
      CurTok(rf).keyword = key;
  END HaveKeyword;

PROCEDURE CurTok(rf: T): Token =
  BEGIN
    IF rf.curTok.type = TokType.None THEN
      GetToken(rf);
    END;
    RETURN rf.curTok;
  END CurTok;

PROCEDURE Oops(rf: T; msg: TEXT) RAISES {RCSError.E} =
  BEGIN
    RAISE RCSError.E(Fmt.Int(rf.line) & ": " & msg);
  END Oops;
*************************************************************************** The Token type. ***************************************************************************

TYPE
  Token = REF RECORD
    type: TokType := TokType.None;
    keyword: Keyword := Keyword.None;
    line: CARDINAL;
    ptr: UNTRACED REF CHAR;
    len: CARDINAL;
  END;

  TokType = {
    Colon,
    Id,		(* also includes Sym *)
    Num,
    Semicolon,
    String,

    Bad,

    EOF,

    None
  };

  Keyword = {
    Access,
    Author,
    Branch,
    Branches,
    Comment,
    Date,
    Desc,
    Expand,
    Head,
    Locks,
    Log,
    Next,
    State,
    Strict,
    Symbols,
    Text,

    None
  };

PROCEDURE KeywordName(key: Keyword): TEXT =
  BEGIN
    CASE key OF
    | Keyword.Access	=> RETURN "access";
    | Keyword.Author	=> RETURN "author";
    | Keyword.Branch	=> RETURN "branch";
    | Keyword.Branches	=> RETURN "branches";
    | Keyword.Comment	=> RETURN "comment";
    | Keyword.Date	=> RETURN "date";
    | Keyword.Desc	=> RETURN "desc";
    | Keyword.Expand	=> RETURN "expand";
    | Keyword.Head	=> RETURN "head";
    | Keyword.Locks	=> RETURN "locks";
    | Keyword.Log	=> RETURN "log";
    | Keyword.Next	=> RETURN "next";
    | Keyword.State	=> RETURN "state";
    | Keyword.Strict	=> RETURN "strict";
    | Keyword.Symbols	=> RETURN "symbols";
    | Keyword.Text	=> RETURN "text";
    | Keyword.None	=> RETURN "none";
    END;
  END KeywordName;

PROCEDURE TokText(tok: Token): TEXT =
  BEGIN
    IF tok.type = TokType.String THEN
      RETURN CText.CopyQuotedMtoT(tok.ptr, tok.len);
    ELSE
      RETURN CText.CopyMtoT(tok.ptr, tok.len);
    END;
  END TokText;

PROCEDURE TokTypeName(type: TokType): TEXT =
  BEGIN
    CASE type OF
    | TokType.Colon		=> RETURN "Colon";
    | TokType.Id		=> RETURN "Id";
    | TokType.Num		=> RETURN "Num";
    | TokType.Semicolon		=> RETURN "Semicolon";
    | TokType.String		=> RETURN "String";
    | TokType.Bad		=> RETURN "Bad";
    | TokType.EOF		=> RETURN "EOF";
    | TokType.None		=> RETURN "None";
    END;
  END TokTypeName;
*************************************************************************** The String type. ***************************************************************************

TYPE
  (* Base classes with common portions of the implementation. *)

  String = RCSString.T OBJECT
    ptr: UNTRACED REF CHAR;
    len: CARDINAL;
  OVERRIDES
    numLines := StrNumLines;
    toText := NIL;	(* Must be overridden. *)
    iterate := NIL;	(* Must be overridden. *)
  END;

  StringIter = RCSString.Iterator OBJECT
    ptr: UNTRACED REF CHAR;
    lim: UNTRACED REF CHAR;
  OVERRIDES
    next := NIL;	(* Must be overridden. *)
  END;

PROCEDURE StrNumLines(s: String): CARDINAL =
  VAR
    ptr := s.ptr;
    lim := s.ptr + s.len;
    numLines: CARDINAL := 0;
  BEGIN
    WHILE ptr < lim DO
      INC(numLines);
      WHILE ptr < lim AND ptr^ # '\n' DO  (* Find the next newline. *)
	INC(ptr);
      END;
      IF ptr = lim THEN EXIT END;
      INC(ptr);
    END;
    RETURN numLines;
  END StrNumLines;
*************************************************************************** Specializations for simple, unquoted strings. ***************************************************************************

TYPE
  SimpleString = String OBJECT OVERRIDES
    toText := SSToText;
    iterate := SSIterate;
  END;

  SimpleStringIter = StringIter OBJECT OVERRIDES
    next := SSNext;
  END;

PROCEDURE SSToText(s: SimpleString): TEXT =
  BEGIN
    RETURN CText.CopyMtoT(s.ptr, s.len);
  END SSToText;

PROCEDURE SSIterate(s: SimpleString): RCSString.Iterator =
  BEGIN
    RETURN NEW(SimpleStringIter, ptr := s.ptr, lim := s.ptr + s.len);
  END SSIterate;

PROCEDURE SSNext(iter: SimpleStringIter; VAR line: RCSString.T): BOOLEAN =
  VAR
    start := iter.ptr;
  BEGIN
    IF iter.ptr >= iter.lim THEN
      RETURN FALSE;
    END;

    WHILE iter.ptr < iter.lim AND iter.ptr^ # '\n' DO
      INC(iter.ptr);
    END;
    IF iter.ptr < iter.lim THEN  (* Include the newline too *)
      INC(iter.ptr);
    END;
    line := NEW(SimpleString, ptr := start, len := iter.ptr-start);
    RETURN TRUE;
  END SSNext;
*************************************************************************** Specializations for strings in which @ characters are doubled. ***************************************************************************

TYPE
  QuotedString = String OBJECT OVERRIDES
    toText := QSToText;
    iterate := QSIterate;
  END;

  QuotedStringIter = StringIter OBJECT OVERRIDES
    next := QSNext;
  END;

PROCEDURE QSToText(s: QuotedString): TEXT =
  BEGIN
    RETURN CText.CopyQuotedMtoT(s.ptr, s.len);
  END QSToText;

PROCEDURE QSIterate(s: QuotedString): RCSString.Iterator =
  BEGIN
    RETURN NEW(QuotedStringIter, ptr := s.ptr, lim := s.ptr + s.len);
  END QSIterate;

PROCEDURE QSNext(iter: QuotedStringIter; VAR line: RCSString.T): BOOLEAN =
  VAR
    start := iter.ptr;
  BEGIN
    IF iter.ptr >= iter.lim THEN
      RETURN FALSE;
    END;

    WHILE iter.ptr < iter.lim AND iter.ptr^ # '\n' DO
      INC(iter.ptr);
    END;
    IF iter.ptr < iter.lim THEN  (* Include the newline too *)
      INC(iter.ptr);
    END;
    line := NEW(QuotedString, ptr := start, len := iter.ptr-start);
    RETURN TRUE;
  END QSNext;
***************************************************************************

BEGIN
  FOR key := FIRST(Keyword) TO LAST(Keyword) DO
    IF key # Keyword.None THEN
      EVAL keyTab.put(KeywordName(key), ORD(key));
    END;
  END;
END RCSFile.