--WaterlilyImpl.mesa
-- Last edited by:
-- Kolling on March 15, 1983 4:23 pm

DIRECTORY

FileIO
USING[Open, OpenFailed],
IO
USING[char, CharProc, Close, CR, CreateOutputStreamToRope, EndOf, EndOfStream,
GetChar, GetIndex, GetInt, GetOutputStreamRope, GetSequence, Handle, int,
LineAtATime, Put, PutChar, PutF, PutRope, Reset, rope, RIS, Signal, SP, TAB, time],
OrderedSymbolTable
USING[CreateTable, EnumerateIncreasing, Initialize, Insert, Lookup,
Table],
Rope
USING[Cat, Fetch, Find, FromChar, Length, ROPE, Substr],
SafeStorage
USING[NewZone],
UserExec
USING[ExecHandle, GetStreams, HistoryEvent, RegisterCommand],
WaterlilyDefs
USING[SymbolTableEntry, SymbolTableKey];


WaterlilyImpl: PROGRAM
IMPORTS FileIO, IO, OST: OrderedSymbolTable, Rope, SafeStorage,
UserExec, Wat: WaterlilyDefs =

BEGIN

--part of this algorithm cribbed from Heckel, Paul. A Technique For Isolating Differences Between Files. Comm. ACM 21,4 (April 1978), 264-268. Pointer to which courtesy of Ed Taft.


TerminatingSignal: SIGNAL = CODE;



WaterlilyProc: SAFE PROCEDURE[event: UserExec.HistoryEvent, exec: UserExec.ExecHandle,
clientData: REF ANY] RETURNS[ok: BOOLEAN, msg: Rope.ROPE] = TRUSTED
BEGIN
ENABLE TerminatingSignal => GOTO done; -- what a crock. where did StopMesa go.....

Alarm: ERROR = CODE;

oldArray: REF FileArray;
newArray: REF FileArray;

FileArray: TYPE = RECORD[entry: SEQUENCE index: [0..LAST[CARDINAL]] OF
FileArrayEntry];
FileArrayEntry: TYPE = RECORD[
symTableKey: Wat.SymbolTableKey,
posInThisFile: INT,
lineNumInOtherFile: INTEGER,
typeOfPntr: {symTable, lineNum}];

totalLinesInNewFile: INTEGER ← 0;
totalLinesInOldFile: INTEGER ← 0;

posInNewFile: INT ← 0;
posInOldFile: INT ← 0;

newFile: IO.Handle;
oldFile: IO.Handle;
difFile: IO.Handle;
newFileName: Rope.ROPENIL;
oldFileName: Rope.ROPENIL;
difFileName: Rope.ROPENIL;

Asterisks: Rope.ROPE = "**************************************";

--these switches are global or local.
FileType: TYPE = {tioga, bravo, unform};
switchNewFileType, switchOldFileType: FileType ← tioga;
switchIgnoreEmptyLinesNewFile, switchIgnoreEmptyLinesOldFile: BOOLEANTRUE;

--these switches are global only.
switchLinesForMatch: INTEGER ← 3;
switchLinesForContext: INTEGER ← 1;
switchPause: BOOLEANTRUE;
switchOverwrite: BOOLEANTRUE;

anyDifferencesSeen: BOOLEANFALSE;
indexN, indexO: INTEGER;

SymbolTableProcedure: TYPE = SAFE PROCEDURE[symbolTableKey: Wat.SymbolTableKey]
RETURNS [stop: BOOLEAN];

duplicateList: REF DuplicateRecord ← NIL;
DuplicateRecord: TYPE = RECORD[lineNum: INTEGER, symbolTableKey:
Wat.SymbolTableKey, posInThisFile: INT, nextDupRec: REF DuplicateRecord];

SetSymbolTableEntry: PROCEDURE [nextLine: Rope.ROPE, newOrOld: {new, old},
lineNumCurrentFile: INTEGER, posInCurrentFile: INT, leadingChar: CHARACTER] =
BEGIN
symbolTableKey: Wat.SymbolTableKey;
IF (symbolTableKey ← OST.Lookup[symbolTable, nextLine]) = NIL
THEN BEGIN
symbolTableKey ← SymbolTableZone.NEW[Wat.SymbolTableEntry ← [
timesInOldFile: (IF newOrOld = old THEN 1 ELSE 0),
timesInNewFile: (IF newOrOld = new THEN 1 ELSE 0),
lineNumberInOldFile: (IF newOrOld = old
THEN lineNumCurrentFile ELSE 0),
lineNumberInNewFile: (IF newOrOld = new
THEN lineNumCurrentFile ELSE 0),
posInOldFile: posInCurrentFile,
posInNewFile: posInCurrentFile,
rope: nextLine,
rbLLink: , rbRLink: , rbColor: ]];
OST.Insert[symbolTable, symbolTableKey, symbolTableKey.rope];
END
ELSE SELECT newOrOld FROM
new => BEGIN IF symbolTableKey.timesInNewFile = 0
THEN BEGIN symbolTableKey.lineNumberInNewFile ←
lineNumCurrentFile;
symbolTableKey.posInNewFile ← posInCurrentFile;
END
ELSE EnterDuplicateRecord[symbolTableKey,
lineNumCurrentFile, posInCurrentFile];
IF symbolTableKey.timesInNewFile < 2
THEN symbolTableKey.timesInNewFile ←
symbolTableKey.timesInNewFile + 1;
END;
ENDCASE => BEGIN IF symbolTableKey.timesInOldFile = 0
THEN BEGIN symbolTableKey.lineNumberInOldFile ←
lineNumCurrentFile;
symbolTableKey.posInOldFile ← posInCurrentFile;
END
ELSE EnterDuplicateRecord[symbolTableKey,
lineNumCurrentFile, posInCurrentFile];
IF symbolTableKey.timesInOldFile < 2
THEN symbolTableKey.timesInOldFile ←
symbolTableKey.timesInOldFile + 1;
END;
END;


EnterDuplicateRecord: PROCEDURE [symbolTableKey: Wat.SymbolTableKey,
lineNumCurrentFile: INTEGER, posInCurrentFile: INT] =
BEGIN
tempDupRec: REF DuplicateRecord ← DuplicateRecordZone.NEW[DuplicateRecord
← [lineNumCurrentFile, symbolTableKey, posInCurrentFile, duplicateList]];
duplicateList ← tempDupRec;
END;


EndOfFile: SIGNAL = CODE;

ReadLineFromFile: PROCEDURE [InputFile: IO.Handle, localSwitchFileType:
FileType, localSwitchIgnoreEmpties: BOOLEAN] RETURNS [line: Rope.ROPE,
char: CHARACTER, pos: INT] =
BEGIN
SkipCR: IO.CharProc = TRUSTED BEGIN RETURN[(char = IO.CR), TRUE]; END;
DO
skipLine: BOOLEAN;
index: INT;
IF InputFile.EndOf[] THEN SIGNAL EndOfFile;
pos ← InputFile.GetIndex[];
line ← InputFile.GetSequence[IO.LineAtATime]; -- late, lamented
[] ← InputFile.GetSequence[SkipCR]; -- IO.GetLine.
IF localSwitchFileType = bravo AND
(NOT((index ← Rope.Find[line, Rope.FromChar['\032], 0, ]) = -1))
THEN line ← Rope.Substr[line, 0, index];
[char, skipLine] ← GetLeadingNonSemiBlankCharacter[line,
localSwitchIgnoreEmpties];
IF NOT skipLine THEN EXIT;
ENDLOOP;
END;


-- this routine has two functions: (1) to get the leading non-sp, non-tab
-- character, and (2) to determine if the line should be skipped.

GetLeadingNonSemiBlankCharacter: PROCEDURE [line: Rope.ROPE,
localSwitchIgnoreEmpties: BOOLEAN] RETURNS [leadingChar: CHARACTER,
skipLine: BOOLEAN] =
BEGIN
index: CARDINAL ← 0;
length: CARDINAL = Rope.Length[line];
char: CHARACTER;
WHILE index < length
DO
char ← Rope.Fetch[line, index];
IF ((char # IO.SP) AND (char # IO.TAB)) THEN GOTO found;
index ← index + 1;
REPEAT found => RETURN[leadingChar: char, skipLine: FALSE];
ENDLOOP;
--here on an "empty" line.
RETURN[leadingChar: IO.SP, skipLine: localSwitchIgnoreEmpties];
END;


LegalCharInFilename: PROCEDURE[char: CHARACTER] RETURNS[BOOLEAN] =
BEGIN
SELECT char FROM
IN ['A..'Z], IN ['a..'z], IN ['0..'9], '+, '-, '., '!, '$, '[, '], '<, '>
=> RETURN[TRUE];
ENDCASE => RETURN[FALSE];
END;


SkipSpacesAndTabs: PROCEDURE[char: CHARACTER, InputStream: IO.Handle]
RETURNS[nextChar: CHARACTER] =
BEGIN
nextChar ← char;
WHILE ((nextChar = IO.SP) OR (nextChar = IO.TAB))
DO nextChar ← InputStream.GetChar[]; ENDLOOP;
END;


TerminalHoHoDisplayOutput: PROCEDURE[rope: Rope.ROPE, pause: BOOLEAN] =
BEGIN
UserExec.GetStreams[exec].out.PutF["%g*n", IO.rope[rope]];
IF pause THEN BEGIN UserExec.GetStreams[exec].out.PutRope["HIT RETURN TO EXIT: "];
[] ← UserExec.GetStreams[exec].in.GetChar[ ! IO.Signal => TRUSTED BEGIN IF ec = Rubout
THEN BEGIN UserExec.GetStreams[exec].in.Reset[]; CONTINUE; END; END];
END;
SIGNAL TerminatingSignal; -- what a crock. where did StopMesa go.....
END;


FinishUp: PROCEDURE[openDifFile, writeAndCloseDifFile: BOOLEAN, difMsg,
displayMsg: Rope.ROPE, pause: BOOLEAN] =
BEGIN
IF openDifFile THEN OpenDifFileAndWriteHeader[];
IF writeAndCloseDifFile
THEN BEGIN
difFile.PutF["%g*n", IO.rope[difMsg]];
difFile.Close[FALSE];
END;
TerminalHoHoDisplayOutput[displayMsg, pause];
END;


IllegalFileName: ERROR = CODE;

BuildDifFileName: PROCEDURE =
BEGIN -- quick and dirty.
length: INT ← Rope.Length[oldFileName];
firstPosofFileName: INT ← 0;
posOfLastDot: INT;
DO
index: INT ← Rope.Find[oldFileName, Rope.FromChar['>], firstPosofFileName, TRUE];
IF index = -1 THEN EXIT;
IF (firstPosofFileName ← index + 1) >= length THEN ERROR IllegalFileName;
ENDLOOP;
posOfLastDot ← Rope.Find[oldFileName, Rope.FromChar['.], firstPosofFileName, TRUE];
difFileName ← Rope.Substr[oldFileName, firstPosofFileName, (IF posOfLastDot = -1 THEN length ELSE posOfLastDot) - firstPosofFileName];
difFileName ← Rope.Cat[difFileName, ".dif"];
END;



OpenDifFileAndWriteHeader: PROCEDURE =
BEGIN -- quick and dirty.
difFile ← FileIO.Open[difFileName, overwrite, (IF switchOverwrite THEN
none ELSE newOnly)
! FileIO.OpenFailed => TRUSTED BEGIN IF why = fileAlreadyExists THEN GOTO fileAlreadyExists; END];
difFile.PutF["*nWaterlily Version 1.13 %g*n*nDifferences between files %g and %g:*n*n",
IO.time[], IO.rope[oldFileName], IO.rope[newFileName]];
EXITS fileAlreadyExists => TRUSTED BEGIN oldFile.Close[FALSE]; newFile.Close[FALSE]; TerminalHoHoDisplayOutput["Dif file already exists.", TRUE]; END;
END
;



WorkingMsg: PROCEDURE =
BEGIN
UserExec.GetStreams[exec].out.PutChar['.];
END;


ReadComDotCmOrPrompt: PROCEDURE =
BEGIN
newFileSeen, oldFileSeen: BOOLEANFALSE;
SyntaxError: ERROR = CODE;

ProcessGlobalSwitches: PROCEDURE[char: CHARACTER] RETURNS[nextChar:
CHARACTER] =
BEGIN
DO
IF char = '/ THEN char ← event.commandLineStream.GetChar[];
SELECT char FROM
'b, 'B => BEGIN switchNewFileType ← bravo; switchOldFileType ← bravo;
END;
't, 'T => BEGIN switchNewFileType ← tioga; switchOldFileType ← tioga;
END;
'u, 'U => BEGIN switchNewFileType ← unform;
switchOldFileType ← unform;
END;
'o, 'O => switchOverwrite ← TRUE;
'p, 'P => switchPause ← TRUE;
'i, 'I => BEGIN switchIgnoreEmptyLinesNewFile ← TRUE;
switchIgnoreEmptyLinesOldFile ← TRUE; END;
'- => BEGIN char ← event.commandLineStream.GetChar[];
SELECT char FROM
'i, 'I => BEGIN switchIgnoreEmptyLinesNewFile ← FALSE;
switchIgnoreEmptyLinesOldFile ← FALSE; END;
'p, 'P => BEGIN switchPause ← FALSE; END;
'o, 'O => switchOverwrite ← FALSE;
ENDCASE => ERROR SyntaxError;
END;
IN ['0..'9] => BEGIN ropeHandle: IO.Handle ←
IO.CreateOutputStreamToRope[];
number: INTEGER;
tempIndex: INTEGER ← 0;
WHILE char IN ['0..'9]
DO
IF tempIndex > 5 THEN ERROR SyntaxError;
ropeHandle.PutChar[char];
tempIndex ← tempIndex + 1;
char ← event.commandLineStream.GetChar[];
ENDLOOP;
number ←
IO.GetInt[IO.RIS[ropeHandle.GetOutputStreamRope[], NIL]];
ropeHandle.Close[];
SELECT char FROM
'm, 'M => BEGIN
IF number <1 THEN ERROR SyntaxError;
switchLinesForMatch ← number;
END;
'c, 'C => switchLinesForContext ← number;
ENDCASE => ERROR SyntaxError;
END;
ENDCASE => ERROR SyntaxError;
SELECT (char ← event.commandLineStream.GetChar[]) FROM
'-, 'b, 'B, 'i, 'I, 'p, 'P, 't, 'T, 'u, 'U, '/, IN ['0..'9] => LOOP;
IO.SP, IO.TAB => BEGIN char ← SkipSpacesAndTabs[char,
event.commandLineStream];
IF (char = '/) THEN LOOP;
END;
ENDCASE => NULL;
RETURN[char];
ENDLOOP;
END;

ProcessLocalSwitches: PROCEDURE [switchFileType: POINTER TO FileType,
switchIgnore: POINTER TO BOOLEAN] RETURNS[nextChar: CHARACTER] =
BEGIN
nextChar ← event.commandLineStream.GetChar[];
DO
SELECT nextChar FROM
'b, 'B => switchFileType^ ← bravo;
't, 'T => switchFileType^ ← tioga;
 'u, 'U => switchFileType^ ← unform;
'i, 'I => switchIgnore^ ← TRUE;
'- => SELECT (nextChar ← event.commandLineStream.GetChar[]) FROM
'i, 'I => switchIgnore^ ← FALSE;
ENDCASE => ERROR SyntaxError;
ENDCASE => ERROR SyntaxError;
SELECT (nextChar ← event.commandLineStream.GetChar[]) FROM
'/ => nextChar ← event.commandLineStream.GetChar[];
'-, 'b, 'B, 'i, 'I, 't, 'T, 'u, 'U => NULL;
ENDCASE => RETURN[nextChar];
ENDLOOP;
END;


OpenFile: PROCEDURE[tempFileName: Rope.ROPE, switchFileType: FileType]
RETURNS[fileSeen: BOOLEAN, fileName: Rope.ROPE, file: IO.Handle] =
BEGIN
file ← FileIO.Open[tempFileName, read, oldOnly, , , switchFileType #
tioga];
fileName ← Rope.Substr[tempFileName, 0, Rope.Length[tempFileName]];
fileSeen ← TRUE;
END;


-- may or may not be given a leading character,
GetFileName: PROCEDURE [handle: IO.Handle, leadingChar: CHARACTER] RETURNS
[tempFileName: Rope.ROPE, nextChar: CHARACTER] =
BEGIN
ropeHandle: IO.Handle ← IO.CreateOutputStreamToRope[];
IF LegalCharInFilename[leadingChar] THEN ropeHandle.PutChar[leadingChar];
WHILE LegalCharInFilename[nextChar ← handle.GetChar[]]
DO ropeHandle.PutChar[nextChar]; ENDLOOP;
tempFileName ← ropeHandle.GetOutputStreamRope[];
ropeHandle.Close[];
END;


ProcessFilename: PROCEDURE[char: CHARACTER] RETURNS[nextChar: CHARACTER] =
BEGIN
tempFileName: Rope.ROPENIL;
IF newFileSeen AND oldFileSeen THEN ERROR SyntaxError;
--guaranteed at least one legal char on entry.
[tempFileName, nextChar] ← GetFileName[event.commandLineStream,
char];
IF NOT oldFileSeen
THEN BEGIN IF nextChar = '/ THEN nextChar ←
ProcessLocalSwitches[@switchOldFileType,
@switchIgnoreEmptyLinesOldFile];
[oldFileSeen, oldFileName, oldFile] ← OpenFile[tempFileName,
switchOldFileType];
END
ELSE BEGIN IF nextChar = '/ THEN nextChar ←
ProcessLocalSwitches[@switchNewFileType,
@switchIgnoreEmptyLinesNewFile];
[newFileSeen, newFileName, newFile] ← OpenFile[tempFileName,
switchNewFileType];
END;
nextChar ← SkipSpacesAndTabs[nextChar, event.commandLineStream];
END;


PromptForFile: PROCEDURE[typeOfFile: Rope.ROPE, switchFileType: FileType]
RETURNS [fileSeen: BOOLEAN, fileName: Rope.ROPE, file: IO.Handle] =
BEGIN
DO
BEGIN
tempFileName: Rope.ROPENIL;
nextChar: CHARACTER;
UserExec.GetStreams[exec].out.PutF["%g file: ", IO.rope[typeOfFile]];
[tempFileName, nextChar] ← GetFileName[UserExec.GetStreams[exec].in, IO.SP];
IF nextChar # IO.CR
THEN BEGIN
UNTIL nextChar = IO.CR
DO nextChar ← UserExec.GetStreams[exec].in.GetChar[];
ENDLOOP;
GOTO err;
END;
IF Rope.Length[tempFileName] = 0 THEN GOTO err;
[fileSeen, fileName, file] ← OpenFile[tempFileName, switchFileType
! FileIO.OpenFailed => TRUSTED BEGIN IF ((why = fileNotFound) OR (why = illegalFileName)) THEN GOTO err; END];
EXIT;
EXITS err => UserExec.GetStreams[exec].out.PutF["%g*n",
IO.rope["File not found or syntax error"]];
END;
ENDLOOP;
END;


fatalMsg: Rope.ROPENIL;
BEGIN
ENABLE BEGIN
IO.EndOfStream, SyntaxError =>
BEGIN fatalMsg ← "Syntax error."; GOTO fatalError; END;
FileIO.OpenFailed =>
TRUSTED BEGIN IF ((why = fileNotFound) OR (why = illegalFileName)) THEN BEGIN fatalMsg ← "File not found or syntax error.";
GOTO fatalError; END; END;
END;
char: CHARACTER ← SkipSpacesAndTabs[IO.SP, event.commandLineStream];
IF (char = '/) THEN char ← ProcessGlobalSwitches[char];
IF LegalCharInFilename[char] THEN char ← ProcessFilename[char];
IF LegalCharInFilename[char] THEN char ← ProcessFilename[char];
char ← SkipSpacesAndTabs[char, event.commandLineStream];
IF ((char # IO.CR) AND (char # ';)) THEN ERROR SyntaxError;
EXITS fatalError => BEGIN IF oldFileSeen THEN oldFile.Close[FALSE];
IF newFileSeen THEN newFile.Close[FALSE];
TerminalHoHoDisplayOutput[fatalMsg, TRUE];
END;
END;
-- PromptForFile does its own error checking since it can recover.
BEGIN ENABLE IO.Signal => TRUSTED BEGIN IF ec = Rubout
THEN BEGIN IF oldFileSeen THEN oldFile.Close[FALSE];
UserExec.GetStreams[exec].in.Reset[];
TerminalHoHoDisplayOutput["XXX
", FALSE]; END; END;
IF NOT oldFileSeen
THEN [oldFileSeen, oldFileName, oldFile] ← PromptForFile["Old",
switchOldFileType];
IF NOT newFileSeen
THEN [newFileSeen, newFileName, newFile] ← PromptForFile["New",
switchNewFileType];
END;
BuildDifFileName[! IllegalFileName => GOTO badDifFileName];
EXITS badDifFileName => BEGIN oldFile.Close[FALSE]; newFile.Close[FALSE];
TerminalHoHoDisplayOutput["Dif file would have illegal name.", TRUE]; END;
END;


-- start here.

SymbolTableZone: ZONE = SafeStorage.NewZone[quantized, , ];
DuplicateRecordZone: ZONE = SafeStorage.NewZone[quantized, , ];
symbolTable: OST.Table;

ok ← TRUE;
msg ← NIL;

OST.Initialize[SymbolTableZone.NEW[Wat.SymbolTableEntry],
SymbolTableZone.NEW[Wat.SymbolTableEntry]];
symbolTable ← OST.CreateTable[header:
SymbolTableZone.NEW[Wat.SymbolTableEntry]];

ReadComDotCmOrPrompt[];
UserExec.GetStreams[exec].out.PutRope["Comparing"];
WorkingMsg[];


BEGIN

ScanDuplicateList: PROCEDURE[array: REF FileArray] =
BEGIN
UNTIL duplicateList = NIL
DO
array[duplicateList.lineNum] ← [symTableKey: duplicateList.symbolTableKey,
posInThisFile: duplicateList.posInThisFile, lineNumInOtherFile: 0, typeOfPntr:
symTable];
duplicateList ← duplicateList.nextDupRec;
ENDLOOP;
END;

--Pass 1.
nextLine: Rope.ROPENIL;
leadingChar: CHARACTER;
DO
[nextLine, leadingChar, posInNewFile] ← ReadLineFromFile[newFile, switchNewFileType,
switchIgnoreEmptyLinesNewFile ! EndOfFile => GOTO donewithfile];
SetSymbolTableEntry[nextLine, new, totalLinesInNewFile, posInNewFile, leadingChar];
totalLinesInNewFile ← totalLinesInNewFile + 1;
REPEAT donewithfile =>
BEGIN newFile.Close[FALSE];
IF totalLinesInNewFile = 0 THEN GOTO emptyfile;
END;
ENDLOOP;
WorkingMsg[];


--Pass 1 and 1/2.
newArray ← NEW[FileArray[totalLinesInNewFile]];

BEGIN
Test: SymbolTableProcedure =
TRUSTED BEGIN
IF (symbolTableKey.timesInNewFile >= 1)
THEN newArray[symbolTableKey.lineNumberInNewFile] ← [symTableKey:
symbolTableKey, posInThisFile: symbolTableKey.posInNewFile, lineNumInOtherFile: 0,
typeOfPntr: symTable];
RETURN[FALSE];
END;
OST.EnumerateIncreasing[symbolTable, Test];
END;

ScanDuplicateList[newArray];
WorkingMsg[];


--Pass 2.
DO
[nextLine, leadingChar, posInOldFile] ← ReadLineFromFile[oldFile, switchOldFileType,
switchIgnoreEmptyLinesOldFile ! EndOfFile => GOTO donewithfile];
SetSymbolTableEntry[nextLine, old, totalLinesInOldFile, posInOldFile, leadingChar];
totalLinesInOldFile ← totalLinesInOldFile + 1;
REPEAT donewithfile =>
BEGIN oldFile.Close[FALSE];
IF totalLinesInOldFile = 0 THEN GOTO emptyfile;
END;
ENDLOOP;
WorkingMsg[];


--Pass 2 and 1/2.
oldArray ← NEW[FileArray[totalLinesInOldFile]];

BEGIN
Test: SymbolTableProcedure =
TRUSTED BEGIN
IF (symbolTableKey.timesInOldFile >= 1)
THEN oldArray[symbolTableKey.lineNumberInOldFile] ← [symTableKey:
symbolTableKey, posInThisFile: symbolTableKey.posInOldFile, lineNumInOtherFile: 0,
typeOfPntr: symTable];
RETURN[FALSE];
END;
OST.EnumerateIncreasing[symbolTable, Test];
END;

ScanDuplicateList[oldArray];
WorkingMsg[];

EXITS emptyfile => FinishUp[openDifFile: (NOT switchPause),
writeAndCloseDifFile: (NOT switchPause),
difMsg: "At least one of these files is effectively empty.",
displayMsg: "At least one of these files is effectively empty.",
pause: switchPause];
END;


--Pass 3.
BEGIN
Test: SymbolTableProcedure =
TRUSTED BEGIN
IF ((symbolTableKey.timesInOldFile = 1) AND
(symbolTableKey.timesInNewFile = 1))
THEN BEGIN
newArray[symbolTableKey.lineNumberInNewFile].typeOfPntr ← lineNum;
newArray[symbolTableKey.lineNumberInNewFile].lineNumInOtherFile ←
symbolTableKey.lineNumberInOldFile;
oldArray[symbolTableKey.lineNumberInOldFile].typeOfPntr ← lineNum;
oldArray[symbolTableKey.lineNumberInOldFile].lineNumInOtherFile ←
symbolTableKey.lineNumberInNewFile;
END;
RETURN[FALSE];
END;

OST.EnumerateIncreasing[symbolTable, Test];
END;
WorkingMsg[];


--Pass 4.
indexN ← 0;
WHILE indexN < totalLinesInNewFile
DO
IF (newArray[indexN].typeOfPntr = lineNum)
THEN BEGIN
indexO ← newArray[indexN].lineNumInOtherFile + 1;
indexN ← indexN + 1;
WHILE ((indexN < totalLinesInNewFile) AND
(indexO < totalLinesInOldFile))
DO IF ((newArray[indexN].typeOfPntr = symTable) AND
(oldArray[indexO].typeOfPntr = symTable) AND
(newArray[indexN].symTableKey = oldArray[indexO].symTableKey))
THEN BEGIN newArray[indexN].typeOfPntr ← lineNum;
oldArray[indexO].typeOfPntr ← lineNum;
newArray[indexN].lineNumInOtherFile ← indexO;
oldArray[indexO].lineNumInOtherFile ← indexN;
END
ELSE IF ((newArray[indexN].typeOfPntr # lineNum) OR
(oldArray[indexO].typeOfPntr # lineNum) OR
(newArray[indexN].lineNumInOtherFile # indexO) OR
(oldArray[indexO].lineNumInOtherFile # indexN))
THEN GOTO endofthisset;
indexN ← indexN + 1;
indexO ← indexO + 1;
REPEAT endofthisset => NULL;
ENDLOOP;
END
ELSE indexN ← indexN + 1;
ENDLOOP;
WorkingMsg[];


--Pass 5.
indexN ← totalLinesInNewFile - 1;
WHILE indexN >= 0
DO
IF (newArray[indexN].typeOfPntr = lineNum)
THEN BEGIN indexO ← newArray[indexN].lineNumInOtherFile - 1;
indexN ← indexN - 1;
WHILE ((indexN >= 0) AND (indexO >= 0))
DO
IF ((newArray[indexN].typeOfPntr = symTable) AND
(oldArray[indexO].typeOfPntr = symTable) AND
(newArray[indexN].symTableKey = oldArray[indexO].symTableKey))
THEN BEGIN newArray[indexN].typeOfPntr ← lineNum;
oldArray[indexO].typeOfPntr ← lineNum;
newArray[indexN].lineNumInOtherFile ← indexO;
oldArray[indexO].lineNumInOtherFile ← indexN;
END
ELSE IF ((newArray[indexN].typeOfPntr # lineNum) OR
(oldArray[indexO].typeOfPntr # lineNum) OR
(newArray[indexN].lineNumInOtherFile # indexO) OR
(oldArray[indexO].lineNumInOtherFile # indexN))
THEN GOTO endofthisset;
indexN ← indexN - 1;
indexO ← indexO - 1;
REPEAT endofthisset => NULL;
ENDLOOP;
END
ELSE indexN ← indexN - 1;
ENDLOOP;
WorkingMsg[];


--Passes 6 and 7.
BEGIN
CancelMatch: PROCEDURE[array1, array2: REF FileArray, totalNumOfLinesFile1,
index: INTEGER] =
BEGIN
UNTIL (array1[index].typeOfPntr = symTable)
DO
array1[index].typeOfPntr ← symTable;
array2[array1[index].lineNumInOtherFile].typeOfPntr ← symTable;
index ← index + 1;
IF ((index >= totalNumOfLinesFile1) OR (array1[index].lineNumInOtherFile
# array1[index - 1].lineNumInOtherFile + 1)) THEN EXIT;
ENDLOOP;
END;

--Pass 6.
IF switchLinesForMatch > 1
THEN BEGIN indexN ← 0;
WHILE (indexN < totalLinesInNewFile)
DO
IF newArray[indexN].typeOfPntr = lineNum
THEN BEGIN oldIndexN: INTEGER ← indexN;
indexN ← indexN + 1;
WHILE ((indexN < totalLinesInNewFile) AND
(newArray[indexN].lineNumInOtherFile = newArray[indexN -
1].lineNumInOtherFile + 1))
DO indexN ← indexN + 1; ENDLOOP;
IF (((indexN - oldIndexN) < switchLinesForMatch) AND
(indexN < totalLinesInNewFile))
THEN CancelMatch[newArray, oldArray,
totalLinesInNewFile, oldIndexN];
END
ELSE indexN ← indexN + 1;
ENDLOOP;
END;
WorkingMsg[];

--Pass 7.
BEGIN
DumpOutDiffAndMoveAhead: PROCEDURE =
BEGIN
index: INTEGER;
LeadingNumber: PROCEDURE[char: CHARACTER, number: INT] = -- must be a better way to do this.
BEGIN
leadingZeroes: CARDINAL ← columns - 1;
tempIndex: INT ← number;
DO
tempIndex ← tempIndex/10;
IF tempIndex = 0 THEN EXIT;
leadingZeroes ← leadingZeroes - 1;
ENDLOOP;
difFile.PutF["%g/", IO.char[char]];
THROUGH [0..leadingZeroes) DO difFile.PutChar['0]; ENDLOOP;
difFile.Put[IO.int[number]];
difFile.PutRope[") "];
END;

IF NOT anyDifferencesSeen
THEN BEGIN OpenDifFileAndWriteHeader[];
difFile.PutF["%g%g*n", IO.rope[Asterisks], IO.rope[Asterisks]];
anyDifferencesSeen ← TRUE;
END;
IF ((indexN >= totalLinesInNewFile) OR (indexO >= totalLinesInOldFile))
THEN BEGIN indexN ← totalLinesInNewFile;
indexO ← totalLinesInOldFile;
END
ELSE IF newArray[indexN].typeOfPntr = lineNum
THEN indexO ← newArray[indexN].lineNumInOtherFile
ELSE indexN ← oldArray[indexO].lineNumInOtherFile;

FOR index IN [startDifO..indexO)
DO LeadingNumber['1, oldArray[index].posInThisFile];
difFile.PutF["%g*n", IO.rope[oldArray[index].symTableKey.rope]]; ENDLOOP;
FOR index IN [indexO..indexO + switchLinesForContext)
WHILE (index < totalLinesInOldFile)
DO LeadingNumber['1, oldArray[index].posInThisFile];
difFile.PutF["%g*n", IO.rope[oldArray[index].symTableKey.rope]]; ENDLOOP;
difFile.PutF["%g*n", IO.rope[Asterisks]];
FOR index IN [startDifN..indexN)
DO LeadingNumber['2, newArray[index].posInThisFile];
difFile.PutF["%g*n", IO.rope[newArray[index].symTableKey.rope]]; ENDLOOP;
FOR index IN [indexN..indexN + switchLinesForContext)
WHILE (index < totalLinesInNewFile)
DO LeadingNumber['2, newArray[index].posInThisFile];
difFile.PutF["%g*n", IO.rope[newArray[index].symTableKey.rope]]; ENDLOOP;
difFile.PutF["%g%g*n", IO.rope[Asterisks], IO.rope[Asterisks]];

index ← indexN + 1;
WHILE ((index < totalLinesInNewFile) AND
(newArray[index].typeOfPntr # symTable))
DO IF (newArray[index].lineNumInOtherFile # newArray[index -
1].lineNumInOtherFile + 1) THEN EXIT;
index ← index + 1;
ENDLOOP;
indexO ← indexO + (index - indexN);
indexN ← index;
startDifN ← indexN;
startDifO ← indexO;
END;

TryToResolveConflicts: PROCEDURE [array1, array2: REF FileArray, index1,
index2: INTEGER] RETURNS[okToDumpDiff: BOOLEAN] =
BEGIN
lastRange1: INTEGER ← index1 + array1[index1].lineNumInOtherFile - index2;
tempIndex: INTEGER;
FOR tempIndex IN [index2..array1[index1].lineNumInOtherFile)
DO IF array2[tempIndex].typeOfPntr = lineNum
THEN BEGIN IF array2[tempIndex].lineNumInOtherFile > lastRange1
THEN CancelMatch[array2, array1, totalLinesInOldFile,
tempIndex]
ELSE BEGIN CancelMatch[array1, array2,
totalLinesInNewFile, index1];
RETURN[okToDumpDiff: FALSE];
END;
END;
ENDLOOP;
RETURN[okToDumpDiff: TRUE];
END;

startDifN: INTEGER;
startDifO: INTEGER;
dumpDiff: BOOLEAN;

columns: [0..255] ← 1;
max: INTMAX[posInNewFile, posInOldFile];
DO
max ← max/10;
IF max = 0 THEN EXIT;
columns ← columns + 1;
ENDLOOP;

indexN ← 0;
indexO ← 0;
WHILE ((indexN < totalLinesInNewFile) AND (indexO < totalLinesInOldFile) AND
(newArray[indexN].typeOfPntr = lineNum) AND (oldArray[indexO].typeOfPntr =
lineNum) AND (newArray[indexN].lineNumInOtherFile = indexO) AND
(oldArray[indexO].lineNumInOtherFile = indexN))
DO
indexN ← indexN + 1;
indexO ← indexO + 1;
ENDLOOP;

startDifN ← indexN;
startDifO ← indexO;
dumpDiff ← FALSE;
WHILE ((indexN < totalLinesInNewFile) AND (indexO < totalLinesInOldFile))
DO IF ((newArray[indexN].typeOfPntr = lineNum) OR
(oldArray[indexO].typeOfPntr = lineNum))
THEN BEGIN
IF ((newArray[indexN].typeOfPntr = lineNum) AND
(oldArray[indexO].typeOfPntr = lineNum))
THEN BEGIN
IF ((newArray[indexN].lineNumInOtherFile = indexO) AND
(oldArray[indexO].lineNumInOtherFile = indexN))
THEN GOTO dumpoutthedifference;
IF (newArray[indexN].lineNumInOtherFile - indexO) >
(oldArray[indexO].lineNumInOtherFile - indexN)
THEN CancelMatch[newArray, oldArray, totalLinesInNewFile,
indexN]
ELSE CancelMatch[oldArray, newArray, totalLinesInOldFile,
indexO];
END;
IF (newArray[indexN].typeOfPntr = lineNum)
THEN dumpDiff ← TryToResolveConflicts[newArray, oldArray, indexN,
indexO]
ELSE dumpDiff ← TryToResolveConflicts[oldArray, newArray, indexO,
indexN];
EXITS dumpoutthedifference => dumpDiff ← TRUE;
END;
IF dumpDiff
THEN BEGIN DumpOutDiffAndMoveAhead[];
dumpDiff ← FALSE;
END
ELSE BEGIN indexN ← indexN + 1;
indexO ← indexO + 1;
END;
ENDLOOP;
IF ((startDifN < totalLinesInNewFile) OR (startDifO < totalLinesInOldFile))
THEN DumpOutDiffAndMoveAhead[];
END;
WorkingMsg[];

IF anyDifferencesSeen
THEN BEGIN tempRope: Rope.ROPE ← Rope.Cat["Differences written on file ",
difFileName, "."];
FinishUp[openDifFile: FALSE, writeAndCloseDifFile: TRUE, difMsg:
"
End of differences seen.", displayMsg: tempRope, pause: FALSE];
END
ELSE FinishUp[openDifFile: FALSE, writeAndCloseDifFile:
FALSE, difMsg: NIL,
displayMsg: "No differences encountered.", pause: switchPause];

END;

EXITS done => NULL;

END;



-- main body of module.

ShortHelpMsg: Rope.ROPE = "Compares two source files.";
LongHelpMsg: Rope.ROPE = "
Waterlily compares two source files. This is the format of its command line:

Waterlily <globalswitches> filename1<localswitches> filename2<localswitches>

where the global switches acceptable are:

t Tioga format files. Only nodes whose Comment property is FALSE are seen. This is the default among t, b, and u.
b Bravo format files. Everything on a line between ^Z and CR is ignored.
u Unformatted files. Neither Tioga nor Bravo, i.e., no filtering.
i Ignore blank and empty lines. Default = TRUE. -i is legal.
o Overwrite an existing dif file. Default = TRUE.
p On empty file or no difs encountered, pause and do not write a dif file. Default = TRUE. -p is legal and means don't pause, but do write a dif file for the empty case.
/#m Number of lines to consider a match. Default = 3.
/#c Number of trailing lines to output for context. Default = 1.

The local switches acceptable are t, b, u, and i.

Examples of command lines are:

Waterlily NewPkgImpl.mesa [Indigo]<Cedar>Pkg>PkgImpl.mesa
Waterlily /b Foo.bravo Foo2.bravo/-i
Wate /6m OldMemo.Bravo/b Memo.tioga
Waterlily (this form will prompt for the filenames.)

The differences found are written on the file filename.dif, where filename is filename1 stripped of any remote path specification.

Only the form of remote path specification which uses square and angle brackets is acceptable; remote path specs using /'s will be rejected.
";

UserExec.RegisterCommand["Waterlily", WaterlilyProc, ShortHelpMsg, LongHelpMsg, ];



END.


Edit Log


Initial: by Karen Kolling: November 13, 1979 12:18 PM.


Change: March 11, 1980 11:40 AM: Version 1.1. increased pages for display in help message from 20 to 25 so top of msg doesn't scroll off screen for larger system fonts. Added output of CR as first character, to avoid mesa image bug that prefixed mesa.typescript with garbage.

Change: May 5, 1980 4:04 PM: Version 1.4. removed quick and dirty that required that each file be read from the disk twice. changed dif file name from waterlily.dif to <firstfilename>.dif. added global switch /p.

Change: May 8, 1980 5:39 PM: Version 1.5. changed array allocations to use segments and then deallocated them so room at end to prevent punt.

Change: January 8, 1981 12:06 PM: Version 1.6. changed the Desc assignments to newArray and oldArray to include the number of lines, for mesa 6.

Change: 22-Mar-82 17:21:45: Version 1.7 for Viewers. made a number of internal changes to use ropes and iostream, took out /h switch since userexec handles help messages diferently. changed switch /b from /b and /-b to /b, /t, /u.

Change: July 7, 1982 12:40 pm: Version 1.8. io => iostream. added "Comparing" message, used new io facility for tioga switch.

Change: October 1, 1982 5:30 pm: Version 1.9. updated long help msg about tioga comment nodes; converted to Cedar 3.4.

Change: November 16, 1982 5:51 pm: Version 1.10. converted to Cedar 3.5.

Change: February 23, 1983 12:28 pm: Version 1.11. position rather than line numbers to dif file.

Change: February 28, 1983 2:21 pm: Version 1.12. converted to Cedar 4.0.

Change: March 15, 1983 1:35 pm: Version 1.13. switch added to allow halt instead of overwriting an existing dif file. Also, if pause is turned off and no differences are seen, doesn't write a dif file.