PDQueueImpl.mesa
Copyright (C) 1984, Xerox Corporation. All rights reserved.
Michael Plass, April 10, 1985 5:28:48 pm PST
DIRECTORY Time, Checksum, Environment, Inline, PDInterpSysCalls, PDQueue, String, Process;
PDQueueImpl: MONITOR
IMPORTS Time, Checksum, Inline, PDInterpSysCalls, String, Process
EXPORTS PDQueue
= BEGIN
Request: TYPE = PDQueue.Request;
readProc: PROC [address: LONG POINTER, nwords: CARDINAL] ← NIL;
writeProc: PROC [address: LONG POINTER, nwords: CARDINAL] ← NIL;
maxStateWords: CARDINAL ← 16*1024;
deferSaving: BOOLEANFALSE;
saveDeferred: BOOLEANFALSE;
state: LONG POINTER TO StringSeqRep ← NIL;
messages: LONG POINTER TO StringSeqRep ← NIL;
StringSeqRep: TYPE = MACHINE DEPENDENT RECORD [
nStrings: CARDINAL,
end: CARDINAL,
mark: CARDINAL,
nMarked: CARDINAL,
nDiscarded: CARDINAL ← 0,
check: CARDINAL,
nWords: CARDINAL,
stringSpace: SEQUENCE COMPUTED CARDINAL OF WORD
];
StringRep: TYPE = RECORD [
nChars: CARDINAL,
char: PACKED ARRAY [0..0) OF CHAR
];
Validate: INTERNAL PROC [s: LONG POINTER TO StringSeqRep] RETURNS [ok: BOOLEAN] = {
nStrings: CARDINAL ← 0;
nMarked: CARDINAL ← 0;
offset: CARDINAL ← 0;
IF s.check # CalculateCheck[s] THEN RETURN [FALSE];
IF s.mark > s.end THEN RETURN [FALSE];
WHILE offset < s.end DO
words: CARDINAL;
nStrings ← nStrings + 1;
IF offset > s.mark AND nMarked = 0 THEN RETURN [FALSE];
IF offset >= s.mark THEN nMarked ← nMarked + 1;
words ← (s[offset]+Environment.bytesPerWord-1)/Environment.bytesPerWord+1;
offset ← offset + words;
ENDLOOP;
IF offset # s.end THEN RETURN [FALSE];
IF nStrings # s.nStrings THEN RETURN [FALSE];
IF nMarked # s.nMarked THEN RETURN [FALSE];
RETURN [TRUE]
};
DiscardMessages: INTERNAL PROC [maxWords: CARDINAL] = {
WHILE messages.end > maxWords DO Discard[messages, 1] ENDLOOP;
};
Discard: INTERNAL PROC [s: LONG POINTER TO StringSeqRep, n: CARDINAL] = {
newStart: CARDINAL ← StringOffset[s, n];
Inline.LongCOPY[from: @(s[newStart]), nwords: s.end-newStart, to: @(s[0])];
s.nStrings ← s.nStrings - n;
s.end ← s.mark ← s.end-newStart;
s.nDiscarded ← s.nDiscarded + n;
};
StringOffset: INTERNAL PROC [s: LONG POINTER TO StringSeqRep, n: CARDINAL] RETURNS [offset: CARDINAL] = {
offset ← 0;
IF n = 0 THEN RETURN;
IF n > s.nStrings THEN ERROR;
FOR i: NAT IN [0..n) DO
offset ← NextStringOffset[s, offset];
ENDLOOP;
};
NextStringOffset: INTERNAL PROC [s: LONG POINTER TO StringSeqRep, offset: CARDINAL] RETURNS [CARDINAL] = INLINE {
IF offset >= s.end THEN ERROR
ELSE {
words: CARDINAL ← (s[offset]+Environment.bytesPerWord-1)/Environment.bytesPerWord+1;
IF words > s.end-offset THEN ERROR;
offset ← offset + words;
RETURN [offset];
};
};
Append: INTERNAL PROC [s: LONG POINTER TO StringSeqRep, string: LONG STRING] = {
body: LONG POINTER TO StringRep;
chars: CARDINALIF string = NIL THEN 0 ELSE string.length;
words: CARDINAL ← (string.length+Environment.bytesPerWord-1)/Environment.bytesPerWord+1;
IF s.mark # s.end THEN ERROR;
IF s.end+words > s.nWords THEN ERROR;
body ← LOOPHOLE[@(s[s.end])];
body.nChars ← chars;
FOR i: NAT IN [0..chars) DO
body.char[i] ← string[i];
ENDLOOP;
IF chars MOD 2 = 1 THEN body.char[chars] ← '\000;
s.mark ← s.end ← s.end+words;
s.nStrings ← s.nStrings + 1;
};
AppendToString: INTERNAL PROC [string: LONG STRING, s: LONG POINTER TO StringSeqRep, offset: CARDINAL] = {
body: LONG POINTER TO StringRep;
IF offset >= s.end THEN ERROR;
IF offset+(s[offset]+Environment.bytesPerWord-1)/Environment.bytesPerWord+1 > s.end THEN ERROR;
body ← LOOPHOLE[@(s[offset])];
FOR i: NAT IN [0..body.nChars) DO
string[string.length] ← body.char[i];
string.length ← string.length + 1;
ENDLOOP;
};
MoveToString: INTERNAL PROC [string: LONG STRING, s: LONG POINTER TO StringSeqRep, offset: CARDINAL] = {
string.length ← 0;
AppendToString[string, s, offset];
};
GetNumberDiscardedMessages: ENTRY PROC RETURNS [CARDINAL] = {
IF messages = NIL THEN Restore[];
RETURN [messages.nDiscarded];
};
GetString: ENTRY PROC [string: LONG STRING, s: LONG POINTER TO StringSeqRep, n: CARDINAL] RETURNS [ok: BOOLEAN] = {
string.length ← 0;
IF n-s.nDiscarded > 32767 THEN RETURN [TRUE];
-- funny condition due to unsigned arithmetic . . . nDiscarded may wrap
IF n-s.nDiscarded >= s.nStrings THEN RETURN [FALSE];
AppendToString[string, s, StringOffset[s, n-s.nDiscarded]];
RETURN [TRUE];
};
MoveMarkedStrings: INTERNAL PROC [from, to: LONG POINTER TO StringSeqRep] = {
nwords: CARDINAL ← from.end-from.mark;
IF to.nWords < to.mark+nwords THEN ERROR;
to.nStrings ← to.nStrings - to.nMarked;
to.nMarked ← 0;
to.end ← to.mark;
Inline.LongCOPY[from: @(from[from.mark]), nwords: nwords, to: @(to[to.mark])];
to.nStrings ← to.nStrings + from.nMarked;
to.nMarked ← from.nMarked;
to.end ← to.end + nwords;
from.nStrings ← from.nStrings-from.nMarked;
from.nMarked ← 0;
from.end ← from.mark;
};
CalculateCheck: INTERNAL PROC [s: LONG POINTER TO StringSeqRep] RETURNS [CARDINAL] = {
cs: CARDINAL ← Checksum.ComputeChecksum[nWords: 5, p: s];
cs ← Checksum.ComputeChecksum[cs: cs, nWords: 1, p: s+6];
RETURN [cs];
};
Save: INTERNAL PROC = {
IF deferSaving THEN {saveDeferred ← TRUE; RETURN};
IF state = NIL THEN ERROR;
DiscardMessages[state.nWords-state.end];
messages.nMarked ← messages.nStrings;
messages.mark ← 0;
MoveMarkedStrings[from: messages, to: state];
state.check ← CalculateCheck[state];
IF NOT Validate[state] THEN ERROR;
IF writeProc # NIL THEN {
usingDisk ← Process.GetCurrent[];
writeProc[state, SIZE[StringSeqRep[state.end]]];
usingDisk ← NIL;
};
MoveMarkedStrings[from: state, to: messages];
messages.nMarked ← 0;
messages.mark ← messages.end;
saveDeferred ← FALSE;
};
Restore: INTERNAL PROC = {
IF state # NIL THEN ERROR;
IF readProc # NIL THEN {
temp: LONG POINTER TO StringSeqRep ← PDInterpSysCalls.AllocateSpace[256];
usingDisk ← Process.GetCurrent[];
readProc[temp, 256];
usingDisk ← NIL;
IF temp.check = CalculateCheck[temp] THEN {
words: CARDINALSIZE[StringSeqRep[temp.end]];
wordsAllocated: CARDINALMAX[maxStateWords, words+255]/256*256;
IF wordsAllocated <= maxStateWords THEN {
state ← PDInterpSysCalls.AllocateSpace[wordsAllocated];
usingDisk ← Process.GetCurrent[];
readProc[state, words];
usingDisk ← NIL;
state.nWords ← wordsAllocated-SIZE[StringSeqRep[0]];
IF Validate[state] THEN {
AllocMessages[];
MoveMarkedStrings[from: state, to: messages];
messages.nMarked ← 0;
messages.mark ← messages.end;
firstUnprintedMessage ← messages.nDiscarded+messages.nStrings-24;
-- it should be ok if this wraps around.
}
ELSE {
PDInterpSysCalls.FreeSpace[state];
state ← NIL
};
};
};
PDInterpSysCalls.FreeSpace[temp];
temp ← NIL;
};
IF state = NIL THEN InternalReset[];
};
AllocMessages: INTERNAL PROC = {
wordsAllocated: CARDINALSIZE[StringSeqRep[state.nWords]];
IF messages # NIL THEN {PDInterpSysCalls.FreeSpace[messages]; messages ← NIL};
messages ← PDInterpSysCalls.AllocateSpace[wordsAllocated];
messages.nWords ← wordsAllocated-SIZE[StringSeqRep[0]];
messages.nStrings ← messages.end ← messages.mark ← messages.nMarked ← messages.nDiscarded ← 0;
};
InternalReset: INTERNAL PROC = {
IF state # NIL THEN {PDInterpSysCalls.FreeSpace[state]; state ← NIL};
state ← PDInterpSysCalls.AllocateSpace[maxStateWords];
state.nWords ← maxStateWords-SIZE[StringSeqRep[0]];
state.nStrings ← state.end ← state.mark ← state.nMarked ← state.nDiscarded ← 0;
AllocMessages[];
};
Reset: PUBLIC ENTRY PROC = {InternalReset[]; Save[]};
newRequest: CONDITION;
stringsPerRequest: CARDINAL = 6;
requestLimit: CARDINAL ← 1000;
Requests are numbered modulo this number.
ReqNumberFromStringNumber: PROC [stringNumber: CARDINAL] RETURNS [requestNumber: CARDINAL] = {
requestNumber ← (stringNumber/stringsPerRequest) MOD requestLimit;
};
DecodeCopies: PROC [tag: LONG STRING] RETURNS [copies: CARDINAL] = {
copies ← ORD[tag[2]]*256+ORD[tag[3]];
};
QueueRequest: PUBLIC ENTRY PROC [request: Request] RETURNS [requestNumber: INT] = {
ENABLE UNWIND => NULL;
tag: LONG STRING ← [4];
wordsNeeded: INT ← 2*(INT[4]+request.fileName.length+request.requestTime.length+request.requestor.length+request.requestorPassword.length+request.separator.length) + stringsPerRequest*(SIZE[StringRep]+1);
IF state = NIL THEN Restore[];
IF state.nStrings >= stringsPerRequest*requestLimit THEN RETURN [-1];
IF state.end+wordsNeeded+10 >= state.nWords THEN RETURN [-1];
requestNumber ← ReqNumberFromStringNumber[state.nStrings+state.nDiscarded];
String.AppendString[tag, " "];
String.AppendChar[tag, VAL[Inline.HighByte[CARDINAL[request.copies]]]];
String.AppendChar[tag, VAL[Inline.LowByte[CARDINAL[request.copies]]]];
Append[state, tag];
Append[state, request.fileName];
Append[state, request.requestTime];
Append[state, request.requestor];
Append[state, request.requestorPassword];
Append[state, request.separator];
IF stringsPerRequest # 6 THEN ERROR;
BEGIN
msg: LONG STRING ← [250];
String.AppendString[msg, request.fileName];
IF request.copies#1 THEN {
String.AppendString[msg, " ("];
String.AppendDecimal[msg, request.copies];
String.AppendString[msg, " copies)"];
};
InternalLogMessage[msg, requestNumber, request.requestor];
END;
Save[];
NOTIFY newRequest;
};
reprintCopies: CARDINAL ← 0;
reprintCancelled: BOOLEANFALSE;
Reprint: PUBLIC ENTRY PROC [copies: CARDINAL] = {
reprintCancelled ← FALSE;
reprintCopies ← reprintCopies + copies;
NOTIFY newRequest;
};
CancelReprint: PUBLIC ENTRY PROC = {
reprintCancelled ← TRUE;
reprintCopies ← 0;
};
ReprintCancelled: PUBLIC PROC RETURNS [BOOLEAN] = {RETURN [reprintCancelled]};
printingSuspended: BOOLEANFALSE;
GetSuspended: PUBLIC ENTRY PROC RETURNS [BOOLEAN] = {RETURN [printingSuspended]};
SetSuspended: PUBLIC ENTRY PROC [suspended: BOOLEAN] RETURNS [old: BOOLEAN] = {
old ← printingSuspended;
printingSuspended ← suspended;
NOTIFY newRequest;
};
abortCurrent: BOOLEAN;
FirstRequest: ENTRY PROC RETURNS [requestNumber: CARDINAL] = {
ENABLE UNWIND => NULL;
IF state = NIL THEN Restore[];
requestNumber ← ReqNumberFromStringNumber[state.nDiscarded];
};
FindRequest: ENTRY PROC [requestNumber: CARDINAL, tag: LONG STRING, request: Request] = {
ENABLE UNWIND => NULL;
stringNumber: CARDINAL ← requestNumber*stringsPerRequest;
offset: CARDINAL ← 0;
IF state = NIL THEN Restore[];
WHILE stringNumber < state.nDiscarded DO stringNumber ← stringNumber + requestLimit*stringsPerRequest ENDLOOP;
IF stringNumber-state.nDiscarded >= state.nStrings THEN RETURN;
offset ← StringOffset[state, stringNumber-state.nDiscarded];
MoveToString[tag, state, offset]; offset ← NextStringOffset[state, offset];
MoveToString[request.fileName, state, offset]; offset ← NextStringOffset[state, offset];
MoveToString[request.requestTime, state, offset]; offset ← NextStringOffset[state, offset];
MoveToString[request.requestor, state, offset]; offset ← NextStringOffset[state, offset];
MoveToString[request.requestorPassword, state, offset]; offset ← NextStringOffset[state, offset];
MoveToString[request.separator, state, offset];
IF stringsPerRequest # 6 THEN ERROR;
};
GetRequest: ENTRY PROC [request: Request] RETURNS [requestNumber: INT, copies: CARDINAL] = {
ENABLE UNWIND => NULL;
tag: LONG STRING ← [4];
IF state = NIL THEN Restore[];
DO
offset: CARDINAL ← 0;
WHILE printingSuspended OR (state.nStrings = 0 AND reprintCopies = 0) DO WAIT newRequest ENDLOOP;
abortCurrent ← FALSE;
reprintCancelled ← FALSE;
IF reprintCopies # 0 THEN {
requestNumber ← -1;
copies ← reprintCopies;
reprintCopies ← 0;
RETURN;
};
requestNumber ← ReqNumberFromStringNumber[state.nDiscarded];
MoveToString[tag, state, offset]; offset ← NextStringOffset[state, offset];
MoveToString[request.fileName, state, offset]; offset ← NextStringOffset[state, offset];
MoveToString[request.requestTime, state, offset]; offset ← NextStringOffset[state, offset];
MoveToString[request.requestor, state, offset]; offset ← NextStringOffset[state, offset];
MoveToString[request.requestorPassword, state, offset]; offset ← NextStringOffset[state, offset];
MoveToString[request.separator, state, offset];
copies ← DecodeCopies[tag];
IF stringsPerRequest # 6 THEN ERROR;
IF tag[0] = ' THEN EXIT;
Discard[state, stringsPerRequest];
WHILE state.nDiscarded >= stringsPerRequest*requestLimit DO
state.nDiscarded ← state.nDiscarded - stringsPerRequest*requestLimit;
ENDLOOP;
ENDLOOP;
};
RemoveRequest: ENTRY PROC [requestNumber: CARDINAL] = {
ENABLE UNWIND => NULL;
IF state = NIL THEN Restore[];
IF requestNumber # ReqNumberFromStringNumber[state.nDiscarded] THEN ERROR;
Discard[state, stringsPerRequest];
WHILE state.nDiscarded >= stringsPerRequest*requestLimit DO
state.nDiscarded ← state.nDiscarded - stringsPerRequest*requestLimit;
ENDLOOP;
Save[];
};
DeferSaving: PUBLIC ENTRY PROC = {
ENABLE UNWIND => NULL;
deferSaving ← TRUE;
};
DoDeferredSave: PUBLIC ENTRY PROC = {
ENABLE UNWIND => NULL;
deferSaving ← FALSE;
IF saveDeferred THEN Save[];
};
DoRequest: PUBLIC PROC [action: PROC [request: Request, requestNumber: CARDINAL, abort: LONG POINTER TO BOOLEAN]] = {
fileName: LONG STRING ← [80];
requestTime: LONG STRING ← [20];
requestor: LONG STRING ← [80];
requestorPassword: LONG STRING ← [80];
separator: LONG STRING ← [80];
requestNumber: INT;
body: LONG POINTER TO StringRep;
copies: CARDINAL;
aborted: BOOLEANFALSE;
[requestNumber, copies] ← GetRequest[[fileName, requestTime, requestor, requestorPassword, separator, 0]];
IF requestNumber < 0 THEN {
LogMessage["Reprint."];
action[[NIL, NIL, NIL, NIL, NIL, copies], NAT.LAST, @abortCurrent ! ABORTED => {aborted ← TRUE; CONTINUE}];
LogMessage["Reprint completed."];
}
ELSE {
body ← LOOPHOLE[@(state[0])];
IF body.nChars # 4 THEN ERROR;
body.char[1] ← 'P;
LogMessage["Started.", requestNumber, requestor];
action[[fileName, requestTime, requestor, requestorPassword, separator, copies], requestNumber, @abortCurrent ! ABORTED => {aborted ← TRUE; CONTINUE}];
IF aborted THEN LogMessage["Aborted.", requestNumber, requestor]
ELSE LogMessage["Completed.", requestNumber, requestor];
RemoveRequest[requestNumber];
};
};
CancelRequest: PUBLIC ENTRY PROC [requestNumber: CARDINAL] RETURNS [ok: BOOLEAN] = {
stringNumber: CARDINAL ← requestNumber*stringsPerRequest;
body: LONG POINTER TO StringRep;
IF state = NIL THEN Restore[];
WHILE stringNumber < state.nDiscarded DO
stringNumber ← stringNumber+requestLimit*stringsPerRequest
ENDLOOP;
IF stringNumber-state.nDiscarded >= state.nStrings THEN RETURN [FALSE];
body ← LOOPHOLE[@(state[StringOffset[state, stringNumber-state.nDiscarded]])];
IF body.nChars # 4 THEN ERROR;
IF body.char[0] # ' THEN RETURN [FALSE];
body.char[0] ← 'X;
IF stringNumber = state.nDiscarded THEN abortCurrent ← TRUE;
Save[];
RETURN [TRUE];
};
RequestStatus: TYPE = PDQueue.RequestStatus;
CheckRequest: PUBLIC PROC [requestNumber: CARDINAL, action: PROC [request: Request, status: RequestStatus]] = {
tag: LONG STRING ← [4];
status: RequestStatus;
fileName: LONG STRING ← [80];
requestTime: LONG STRING ← [20];
requestor: LONG STRING ← [80];
requestorPassword: LONG STRING ← [80];
separator: LONG STRING ← [80];
tag.length ← 0;
FindRequest[requestNumber, tag, [fileName, requestTime, requestor, requestorPassword, separator, 0]];
IF tag.length = 0 THEN status ← notFound
ELSE IF tag[0] = 'X THEN status ← canceled
ELSE IF tag[1] = 'P THEN status ← printing
ELSE status ← waiting;
action[[fileName, requestTime, requestor, requestorPassword, separator, DecodeCopies[tag]], status];
};
CountRequests: PUBLIC ENTRY PROC RETURNS [requestCount: NAT] = {
IF state = NIL THEN Restore[];
requestCount ← state.nStrings/stringsPerRequest;
};
EnumerateRequests: PUBLIC PROC [action: PROC [requestNumber: CARDINAL, request: Request, status: RequestStatus] RETURNS [continue: BOOLEAN]] = {
firstRequest: NAT ← FirstRequest[];
nRequests: NAT ← CountRequests[];
requestNumber: NAT ← firstRequest MOD requestLimit;
THROUGH [0..nRequests) DO
continue: BOOLEANTRUE;
myAction: PROC [request: Request, status: RequestStatus] = {
IF status = printing OR status = waiting THEN continue ← action[requestNumber, request, status];
};
CheckRequest[requestNumber MOD requestLimit, myAction];
IF NOT continue THEN EXIT;
requestNumber ← (requestNumber + 1) MOD requestLimit;
ENDLOOP;
};
RegisterDisk: PUBLIC ENTRY PROC [read, write: PROC [address: LONG POINTER, nwords: CARDINAL], maxWords: CARDINAL] = {
IF state # NIL THEN {
PDInterpSysCalls.FreeSpace[state];
state ← NIL;
};
readProc ← read;
writeProc ← write;
maxStateWords ← maxWords;
};
usingDisk: PROCESSNIL;
Records the process using the disk under the monitor, to avoid deadlocks in case of disk errors.
LogMessage: PUBLIC PROC [message: LONG STRING, requestNumber: INT ← -1, userName: LONG STRINGNIL] = {
IF usingDisk = Process.GetCurrent[] THEN {
IF writeLineProc = NIL THEN ERROR;
Remote debug if it ever gets this bad.
writeLineProc["Help!" ! ABORTED => CONTINUE];
writeLineProc[message ! ABORTED => CONTINUE];
}
ELSE LogMessageEntry[message, requestNumber, userName];
};
LogMessageEntry: ENTRY PROC [message: LONG STRING, requestNumber: INT ← -1, userName: LONG STRINGNIL] = {
InternalLogMessage[message, requestNumber, userName];
};
InternalLogMessage: INTERNAL PROC [message: LONG STRING, requestNumber: INT ← -1, userName: LONG STRINGNIL] = {
msg: LONG STRING ← [250];
Time.AppendCurrent[msg];
msg[msg.length] ← ' ; msg.length ← msg.length + 1;
IF requestNumber >= 0 THEN {
String.AppendChar[msg, 'R];
FOR i: CARDINAL ← requestLimit/10, i/10 WHILE i#0 DO
String.AppendDecimal[msg, (requestNumber/i) MOD 10];
ENDLOOP;
String.AppendChar[msg, ' ];
};
String.AppendString[msg, message];
IF userName#NIL THEN {
String.AppendString[msg, " ("];
String.AppendString[msg, userName];
String.AppendChar[msg, ')];
};
IF state = NIL THEN Restore[];
DiscardMessages[messages.nWords-SIZE[StringBody[msg.length]]];
Append[messages, msg];
TryToPrintMessages[];
};
loggingMessages: BOOLEANFALSE;
firstUnprintedMessage: CARDINAL ← 0;
writeLineProc: PROC[LONG STRING] ← NIL;
RegisterTTY: PUBLIC ENTRY PROC [writeLine: PROC[LONG STRING]] = {
writeLineProc ← writeLine;
TryToPrintMessages[];
};
TryToPrintMessages: INTERNAL PROC = {
msg: LONG STRING ← [250];
GetMsg: INTERNAL PROC [n: CARDINAL] RETURNS [ok: BOOLEAN] = {
msg.length ← 0;
IF n-messages.nDiscarded > 32767 THEN RETURN [TRUE];
-- funny condition due to unsigned arithmetic . . . nDiscarded may wrap
IF n-messages.nDiscarded >= messages.nStrings THEN RETURN [FALSE];
AppendToString[msg, messages, StringOffset[messages, n-messages.nDiscarded]];
RETURN [TRUE];
};
IF NOT loggingMessages OR writeLineProc = NIL THEN RETURN;
FOR i: CARDINAL ← firstUnprintedMessage, i+1 UNTIL NOT GetMsg[i] DO
writeLineProc[msg ! ABORTED => CONTINUE];
firstUnprintedMessage ← i+1;
ENDLOOP;
};
SetLogState: PUBLIC ENTRY PROC [logging: BOOLEAN] RETURNS [old: BOOLEAN] = {
old ← loggingMessages;
loggingMessages ← logging;
IF messages # NIL THEN TryToPrintMessages[];
};
EnumerateMessages: PUBLIC PROC [action: PROC[message: LONG STRING] RETURNS [continue: BOOLEAN]] = {
msg: LONG STRING ← [250];
FOR i: CARDINAL ← GetNumberDiscardedMessages[], i+1 UNTIL NOT GetString[msg, messages, i] DO
IF NOT action[msg].continue THEN EXIT;
ENDLOOP;
};
END.