MBOutput.mesa
Edited by Sandman on 6-Aug-81 15:41:43
Edited by Lewis on 25-Sep-81 15:01:46
Edited by Levin on April 5, 1983 3:21 pm
DIRECTORY
BcdOps USING [MTHandle],
BootFile
USING [
Entry, Header, maxEntriesPerHeader, maxEntriesPerTrailer, Trailer, currentVersion],
Environment USING [bytesPerPage, wordsPerPage],
Inline USING [HighHalf, LongMult, LowHalf],
LongString USING [AppendString],
MB USING [BHandle, DoAllModules, DumpSegs, Handle, StartControlLink, Zero],
MBOut USING [CR, FileName, NumberFormat, Char, Line, LongNumber, Number, Spaces, Text],
MBStorage USING [Pages, FreePages],
MBTTY USING [Handle, PutLine, PutString],
MBVM
USING [
Base, CodeSeg, CopyRead, CopyWrite, DataSeg, FileSeg, GetPage, ReleaseCodeSegs, Seg, SortSegs, Write],
PageMap USING [Flags, flagsClean, flagsWriteProtected, Value],
PrincOps USING [GlobalFrame],
Segments
USING [
FHandle, GetFileLength, NewFile, Read, ReleaseFile, SegmentAddress, SetFileLength, SetFileTimes, SHandle, SwapIn, Unlock, Write],
StartList USING [Base, Entry, Index, StartIndex, SwapUnitIndex, SwapUnitInfo],
Streams
USING [
CreateStream, Destroy, Handle, GetBlock, GetIndex, GetLength, PutBlock, PutWord, Read, SetIndex, Write],
Time USING [Packed];
MBOutput:
PROGRAM
IMPORTS
Inline, MB, MBOut, MBStorage, MBTTY, MBVM, Segments, Streams, String: LongString
EXPORTS MB =
BEGIN
OPEN MBVM;
wordsPerPage: CARDINAL = Environment.wordsPerPage;
bytesPerPage: CARDINAL = Environment.bytesPerPage;
data: MB.Handle ← NIL;
header: LONG POINTER TO BootFile.Header ← NIL;
trailer: LONG POINTER TO BootFile.Trailer ← NIL;
nEntries, currentEntry: CARDINAL;
filePage: MBVM.Base;
trailerIndex: LONG CARDINAL;
EtherHeader:
TYPE =
MACHINE
DEPENDENT
RECORD [
version(0): CARDINAL ← 1,
mustBeZero(1): LONG CARDINAL ← 0,
createTime(3): BCPLTime
the following are implicit
name(5): StringBody,
fill(5+WordsForString[name]): ARRAY [5+WordsForString[name]..wordsPerPage) OF WORD ← ALL[0]
];
BCPLTime: TYPE = MACHINE DEPENDENT RECORD [high, low: CARDINAL];
InitOutput: PUBLIC PROC [h: MB.Handle] = {data ← h};
FinishOutput:
PUBLIC
PROC = {
IF header # NIL THEN {MBStorage.FreePages[header]; header ← NIL};
IF trailer # NIL THEN {MBStorage.FreePages[trailer]; trailer ← NIL};
IF data.bootStream #
NIL
THEN {
Streams.SetIndex[data.bootStream, Streams.GetLength[data.bootStream]];
Streams.Destroy[data.bootStream];
data.bootStream ← NIL;
};
MBVM.ReleaseCodeSegs[];
data ← NIL;
};
Boot file (not germ) output
WriteBootFile:
PUBLIC
PROC = {
tty: MBTTY.Handle = data.ttyHandle;
segs: LONG DESCRIPTOR FOR ARRAY OF MBVM.Seg ← MBVM.SortSegs[];
MBTTY.PutString[tty, "Writing boot file..."L];
InitializeBootFile[];
WriteVM[data.bootStream, segs ! UNWIND => MBStorage.FreePages[BASE[segs]]];
MBStorage.FreePages[BASE[segs]];
FinalizeBootFile[];
MBTTY.PutLine[tty, "finished writing."L];
IF data.etherFormat THEN MakeEtherFile[];
};
InitializeBootFile:
PROC = {
name: STRING ← [40];
bootFile: Segments.FHandle;
String.AppendString[name, data.output];
bootFile ← Segments.NewFile[name, Segments.Write];
Segments.SetFileLength[bootFile, Inline.LongMult[data.nFilePages, bytesPerPage]
! UNWIND => Segments.ReleaseFile[bootFile]
];
data.bootStream ← Streams.CreateStream[bootFile, Streams.Write];
Segments.SetFileTimes[file: bootFile, create: data.buildTime];
Streams.SetIndex[data.bootStream, bytesPerPage]; -- skip over header page
data.bootHeader ← header ← MBStorage.Pages[1];
MB.Zero[header, wordsPerPage];
header^ ← BootFile.Header[
creationDate: data.buildTime, pStartListHeader: data.header.table,
inLoadMode: load, continuation: [vp: initial[mdsi: [data.mdsBase/256],
destination: MB.StartControlLink[]]], countData: data.nBootPages, entries:
];
trailer ← NIL;
nEntries ← BootFile.maxEntriesPerHeader;
currentEntry ← 0;
filePage ← 2;
};
FinalizeBootFile:
PROC = {
length: LONG CARDINAL = Streams.GetIndex[data.bootStream];
Streams.SetIndex[data.bootStream, 0];
[] ← Streams.PutBlock[data.bootStream, header, wordsPerPage];
Streams.SetIndex[data.bootStream, length];
Streams.Destroy[data.bootStream];
data.bootStream ← NIL;
};
WriteVM:
PROC [stream: Streams.Handle, segs:
LONG
DESCRIPTOR
FOR
ARRAY
OF
MBVM.Seg] = {
OPEN MBOut;
tty: MBTTY.Handle = data.ttyHandle;
i: CARDINAL;
seg: MBVM.Seg;
scriptBase: StartList.Base = data.scriptBase;
index: StartList.Index ← StartList.StartIndex;
CR[]; CR[];
Line["BOOT FILE MAP"L];
CR[];
Line[" Bootloaded Memory"L];
Line[" File VM Map Type"L];
Line[" Page Address Flags"L];
Number[1, [8,FALSE,FALSE,6]]; -- header page
Line[" Header Page"L];
MBTTY.PutString[tty, "bootloaded memory..."L];
FOR i
IN [0..
LENGTH[segs])
DO
seg ← segs[i];
IF ~seg.bootLoaded THEN LOOP;
WITH s: seg
SELECT
FROM
data => WriteDataSeg[stream, @s];
code => WriteCodeSeg[stream, @s];
file => WriteFileSeg[stream, @s];
ENDCASE;
ENDLOOP;
IF trailer # NIL THEN [] ← WriteTrailerPage[stream];
CR[];
Line[" Nonresident Memory"L];
Line[" File VM Pages Type Source[base,pages]"L];
Line[" Page Address"L];
MBTTY.PutString[tty, "nonresident memory..."L];
FOR i
IN [0..
LENGTH[segs])
DO
info: StartList.SwapUnitInfo;
seg ← segs[i];
info ← scriptBase[seg.index].info;
WITH s: seg
SELECT
FROM
data =>
IF info.state # resident THEN AppendDataSeg[stream, @s]
ELSE data.nResidentPages ← data.nResidentPages + 1;
code => AppendCodeSeg[stream, @s];
file =>
IF info.state # resident THEN AppendFileSeg[stream, @s]
ELSE data.nResidentPages ← data.nResidentPages + 1;
ENDCASE;
ENDLOOP;
WriteDataSeg:
PROC [stream: Streams.Handle, seg:
MBVM.DataSeg] = {
lp: LONG POINTER;
v: PageMap.Value ← MapValueForSeg[seg];
FOR page:
CARDINAL
IN [seg.base..seg.base+seg.pages)
DO
IF page = 376B THEN LOOP;
EnterPage[page: page, value: v, stream: stream];
data.nBootLoadedPages ← data.nBootLoadedPages + 1;
BootPageToLoadmap[filePage: filePage, vmPage: page, flags: v.flags, type: "data"L];
lp ← MBVM.GetPage[page];
IF lp = NIL THEN WriteEmptyPage[stream] ELSE WritePage[stream, lp];
ENDLOOP;
WriteCodeSeg:
PROC [stream: Streams.Handle, seg:
MBVM.CodeSeg] = {
page: CARDINAL;
nUnits: CARDINAL ← IF seg.sph = NIL THEN 1 ELSE seg.sph.length;
index: StartList.SwapUnitIndex ← seg.index;
v: PageMap.Value ← MapValueForSeg[seg];
scriptBase: StartList.Base = data.scriptBase;
lp: LONG POINTER;
su: POINTER TO swapUnit StartList.Entry;
lp ← OpenSegForTransfer[seg.segment];
page ← seg.base;
FOR i:
CARDINAL
IN [0..nUnits)
DO
su ← @scriptBase[index];
FOR j:
CARDINAL
IN [0..su.pages)
DO
IF scriptBase[su.parent].bootLoaded
THEN {
EnterPage[page: page, value: v, stream: stream];
data.nBootLoadedPages ← data.nBootLoadedPages + 1;
BootPageToLoadmap[filePage: filePage, vmPage: page, flags: v.flags, type: "code"L];
WritePage[stream, lp]};
lp ← lp + wordsPerPage;
page ← page + 1;
ENDLOOP;
index ← index + SIZE[swapUnit StartList.Entry];
ENDLOOP;
CloseSegAfterTransfer[seg.segment];
};
WriteFileSeg:
PROC [stream: Streams.Handle, seg:
MBVM.FileSeg] = {
v: PageMap.Value ← MapValueForSeg[seg];
lp: LONG POINTER ← OpenSegForTransfer[seg.segment];
FOR page:
CARDINAL
IN [seg.base..seg.base+seg.pages)
DO
EnterPage[page: page, value: v, stream: stream];
data.nBootLoadedPages ← data.nBootLoadedPages + 1;
BootPageToLoadmap[filePage: filePage, vmPage: page, flags: v.flags, type: "file"L];
WritePage[stream, lp];
lp ← lp + wordsPerPage;
ENDLOOP;
CloseSegAfterTransfer[seg.segment];
};
AppendDataSeg:
PROC [stream: Streams.Handle, seg:
MBVM.DataSeg] = {
lp: LONG POINTER;
SegToLoadmap[seg, filePage, seg.pages];
FOR page:
CARDINAL
IN [seg.base..seg.base+seg.pages)
DO
lp ← MBVM.GetPage[page];
IF lp = NIL THEN WriteEmptyPage[stream] ELSE WritePage[stream, lp];
ENDLOOP;
AppendCodeSeg:
PROC [stream: Streams.Handle, seg:
MBVM.CodeSeg] = {
lp: LONG POINTER;
nUnits: CARDINAL = (IF seg.sph = NIL THEN 1 ELSE seg.sph.length);
index: StartList.SwapUnitIndex;
scriptBase: StartList.Base = data.scriptBase;
su: POINTER TO swapUnit StartList.Entry;
pagesToWrite: CARDINAL ← 0;
first: BOOL ← TRUE;
index ← seg.index;
FOR i:
CARDINAL
IN [0..nUnits)
DO
su ← @scriptBase[index];
IF su.info.state ~= resident THEN pagesToWrite ← pagesToWrite + su.pages;
index ← index + SIZE[swapUnit StartList.Entry];
ENDLOOP;
IF pagesToWrite = 0 THEN RETURN;
lp ← OpenSegForTransfer[seg.segment];
index ← seg.index;
SegToLoadmap[seg, filePage, pagesToWrite];
FOR i:
CARDINAL
IN [0..nUnits)
DO
su ← @scriptBase[index];
IF data.debug
AND su.info.state # resident
THEN {
IF first THEN {MBOut.Spaces[26]; MBOut.Text["SwapUnits["L]; first ← FALSE}
ELSE MBOut.Text[", "L];
MBOut.Number[index, [8,FALSE,FALSE,1]]};
FOR j:
CARDINAL
IN [0..su.pages)
DO
IF su.info.state # resident THEN WritePage[stream, lp]
ELSE data.nResidentPages ← data.nResidentPages + 1;
lp ← lp + wordsPerPage;
ENDLOOP;
index ← index + SIZE[swapUnit StartList.Entry];
ENDLOOP;
IF --data.debug AND-- ~first THEN {MBOut.Char[']]; MBOut.CR[]};
CloseSegAfterTransfer[seg.segment];
};
AppendFileSeg:
PROC [stream: Streams.Handle, seg:
MBVM.FileSeg] = {
lp: LONG POINTER;
SegToLoadmap[seg, filePage, seg.pages];
IF data.debug
THEN {
index: StartList.SwapUnitIndex ← seg.index;
MBOut.Spaces[26]; MBOut.Text["SwapUnits["L];
FOR i:
CARDINAL
IN [0..seg.nUnits)
DO
MBOut.Number[index, [8,FALSE,FALSE,1]];
IF i ~= seg.nUnits - 1 THEN MBOut.Text[", "L];
index ← index + SIZE[swapUnit StartList.Entry];
ENDLOOP;
MBOut.Char[']]; MBOut.CR[]};
lp ← OpenSegForTransfer[seg.segment];
FOR i:
CARDINAL
IN [0..seg.pages)
DO
WritePage[stream, lp]; lp ← lp + wordsPerPage;
ENDLOOP;
CloseSegAfterTransfer[seg.segment];
};
Germ file output
GermMDS: CARDINAL = 37000B;
BootXferLocation: POINTER = LOOPHOLE[1376B];
GermPageCountLocation: POINTER = LOOPHOLE[1377B];
WriteGermFile:
PUBLIC
PROC = {
tty: MBTTY.Handle = data.ttyHandle;
segs: LONG DESCRIPTOR FOR ARRAY OF MBVM.Seg ← MBVM.SortSegs[];
IF data.debug THEN MB.DumpSegs[segs, "WRITING GERM"L];
MBTTY.PutString[tty, "Writing germ file..."L];
InitializeGermFile[];
data.nFilePages ← data.nResidentPages ← data.nBootLoadedPages ←
WriteGerm[data.bootStream, segs ! UNWIND => MBStorage.FreePages[BASE[segs]]];
MBStorage.FreePages[BASE[segs]];
FinalizeGermFile[];
MBTTY.PutLine[tty, "finished writing."L];
IF data.etherFormat THEN MakeEtherFile[];
};
InitializeGermFile:
PROC = {
name: STRING ← [40];
bootFile: Segments.FHandle;
String.AppendString[name, data.output];
bootFile ← Segments.NewFile[name, Segments.Write];
data.bootStream ← Streams.CreateStream[bootFile, Streams.Write];
Segments.SetFileTimes[file: bootFile, create: data.buildTime];
filePage ← 1;
};
FinalizeGermFile:
PROC = {
Streams.Destroy[data.bootStream];
data.bootStream ← NIL;
};
WriteGerm:
PROC [stream: Streams.Handle, segs:
LONG
DESCRIPTOR
FOR
ARRAY
OF
MBVM.Seg]
RETURNS [germPages: CARDINAL] = {
OPEN MBOut;
relocationPages: CARDINAL;
[germPages, relocationPages] ← RelocateGerm[segs];
CR[]; CR[];
Line["GERM FILE MAP"L];
CR[];
Line[" File VM Map Type"L];
Line[" Page Address Flags"L];
FOR i:
CARDINAL
IN [0..
LENGTH[segs])
DO
WITH s: segs[i]
SELECT
FROM
data => WriteGermData[stream, @s, relocationPages];
code => WriteGermCode[stream, @s, relocationPages];
file => ERROR;
ENDCASE;
ENDLOOP;
RelocateGerm:
PROC [segs:
LONG
DESCRIPTOR
FOR
ARRAY
OF
MBVM.Seg]
RETURNS [germPages: CARDINAL, relocationPages: CARDINAL] = {
codeRelocation:
LONG
CARDINAL =
Inline.LongMult[(relocationPages ← GermMDS - data.mdsBase), wordsPerPage];
RelocateOneModule:
PROC [loadee:
MB.BHandle, mth: BcdOps.MTHandle]
RETURNS [
BOOL] = {
gf: PrincOps.GlobalFrame;
MBVM.CopyRead[from: loadee.mt[mth.gfi].frame, to: @gf, nwords: SIZE[PrincOps.GlobalFrame]];
gf.code.longbase ← gf.code.longbase + codeRelocation;
MBVM.CopyWrite[to: loadee.mt[mth.gfi].frame, from: @gf, nwords: SIZE[PrincOps.GlobalFrame]];
RETURN[FALSE]
};
[] ← MB.DoAllModules[RelocateOneModule];
germPages ← 0;
FOR i: CARDINAL IN [0..LENGTH[segs]) DO germPages ← germPages + segs[i].pages; ENDLOOP;
MBVM.Write[p: BootXferLocation, v: MB.StartControlLink[]];
MBVM.Write[p: GermPageCountLocation, v: germPages];
};
WriteGermData:
PROC [stream: Streams.Handle, seg:
MBVM.DataSeg, relocationPages:
CARDINAL] = {
lp: LONG POINTER;
v: PageMap.Value ← MapValueForSeg[seg];
FOR page:
CARDINAL
IN [seg.base..seg.base+seg.pages)
DO
BootPageToLoadmap[
filePage: filePage, vmPage: page+relocationPages, flags: v.flags, type: "data"L];
lp ← MBVM.GetPage[page];
IF lp = NIL THEN WriteEmptyPage[stream] ELSE WritePage[stream, lp];
ENDLOOP;
WriteGermCode:
PROC [stream: Streams.Handle, seg:
MBVM.CodeSeg, relocationPages:
CARDINAL] = {
v: PageMap.Value ← MapValueForSeg[seg];
lp: LONG POINTER ← OpenSegForTransfer[seg.segment];
SegmentSourceToLoadmap[seg]; MBOut.Char[':]; MBOut.CR[];
FOR page:
CARDINAL
IN [seg.base..seg.base+seg.pages)
DO
BootPageToLoadmap[
filePage: filePage, vmPage: page+relocationPages, flags: v.flags, type: "code"L];
WritePage[stream, lp];
lp ← lp + wordsPerPage;
ENDLOOP;
CloseSegAfterTransfer[seg.segment];
};
Subroutines
MapValueForSeg:
PROC [seg:
MBVM.Seg]
RETURNS [PageMap.Value] = {
flags: PageMap.Flags ←
IF ~seg.info.readOnly THEN PageMap.flagsClean ELSE PageMap.flagsWriteProtected;
reserved = FALSE (0) on a D0 means don't log single bit memory error
RETURN[PageMap.Value[logSingleError: FALSE, flags: flags, realPage: 0]]
};
OpenSegForTransfer:
PROC [seg: Segments.SHandle]
RETURNS [lp:
LONG
POINTER] = {
Segments.SwapIn[seg];
lp ← Segments.SegmentAddress[seg];
};
CloseSegAfterTransfer:
PROC [seg: Segments.SHandle] =
INLINE {Segments.Unlock[seg]};
EnterPage:
PROC [page:
MBVM.Base, value: PageMap.Value, stream: Streams.Handle] = {
entries: LONG POINTER TO ARRAY [0..0) OF BootFile.Entry;
IF currentEntry = nEntries THEN AddTrailerPage[stream];
entries ←
IF nEntries = BootFile.maxEntriesPerTrailer THEN @trailer.entries ELSE @header.entries;
entries[currentEntry] ← [page: page, value: value];
currentEntry ← currentEntry + 1;
};
AddTrailerPage:
PROC [stream: Streams.Handle] = {
IF trailer # NIL THEN trailerIndex ← WriteTrailerPage[stream]
ELSE {
create trailer page
trailer ← MBStorage.Pages[1];
trailerIndex ← Streams.GetIndex[stream];
};
Streams.SetIndex[stream, trailerIndex+bytesPerPage];
MB.Zero[trailer, wordsPerPage];
trailer.version ← BootFile.currentVersion;
nEntries ← BootFile.maxEntriesPerTrailer;
currentEntry ← 0;
MBOut.Number[filePage, [8,FALSE,FALSE,6]];
MBOut.Line[" Trailer Page"L];
filePage ← filePage + 1;
};
WriteTrailerPage:
PROC [stream: Streams.Handle]
RETURNS [index:
LONG
CARDINAL] = {
index ← Streams.GetIndex[stream];
Streams.SetIndex[stream, trailerIndex];
[] ← Streams.PutBlock[stream, trailer, wordsPerPage];
Streams.SetIndex[stream, index];
};
WritePage:
PROC [s: Streams.Handle, p:
LONG
POINTER] = {
filePage ← filePage + 1;
IF Streams.PutBlock[s, p, wordsPerPage] # wordsPerPage THEN ERROR;
};
WriteEmptyPage:
PROC [s: Streams.Handle] = {
filePage ← filePage + 1;
THROUGH [0..wordsPerPage) DO Streams.PutWord[s, 0] ENDLOOP;
};
BootPageToLoadmap:
PROC [filePage, vmPage:
MBVM.Base, flags: PageMap.Flags, type:
STRING] = {
OPEN MBOut;
Number[filePage, [8,FALSE,FALSE,6]];
Spaces[2];
LongNumber[(LONG[vmPage] * wordsPerPage), [8,FALSE,FALSE,8]];
Spaces[3];
Char[IF flags.writeProtected THEN 'W ELSE ' ];
Char[IF flags.dirty THEN 'D ELSE ' ];
Char[IF flags.referenced THEN 'R ELSE ' ];
Spaces[5];
Text[type];
CR[];
};
SegToLoadmap:
PROC [s:
MBVM.Seg, backingPage:
MBVM.Base, pages:
CARDINAL] = {
OPEN MBOut;
octal: NumberFormat = NumberFormat[8,FALSE,FALSE,1];
Number[backingPage, [8,FALSE,FALSE,6]];
Spaces[2];
LongNumber[LONG[s.base]*wordsPerPage, [8,FALSE,FALSE,8]];
Spaces[2];
Number[pages, [8,FALSE,FALSE,4]];
Spaces[4];
WITH seg: s
SELECT
FROM
data => Text["data"L];
code => {Text["code"L]; Spaces[3]; SegmentSourceToLoadmap[s]};
file => {Text["file"L]; Spaces[3]; SegmentSourceToLoadmap[s]};
ENDCASE;
CR[];
};
SegmentSourceToLoadmap:
PROC [seg:
MBVM.Seg] = {
OPEN MBOut;
octal: NumberFormat = NumberFormat[8,FALSE,FALSE,1];
file: Segments.FHandle;
fileBase: CARDINAL;
WITH s: seg
SELECT
FROM
code => {file ← s.file; fileBase ← s.fileBase};
file => {file ← s.file; fileBase ← s.fileBase};
ENDCASE;
FileName[file]; Char['[]; Number[fileBase, octal];
Char[',]; Number[seg.pages, octal]; Char[']];
};
MakeEtherFile:
PROC = {
tty: MBTTY.Handle = data.ttyHandle;
name: STRING ← [40];
bufferPages: CARDINAL = 50;
inFile, outFile: Segments.FHandle;
inStream, outStream: Streams.Handle ← NIL;
buffer: LONG POINTER ← MBStorage.Pages[bufferPages];
PutEtherHeader:
PROC [s: Streams.Handle] = {
etherHeader: EtherHeader ← [createTime: MesaToBCPLTime[data.buildTime]];
[] ← Streams.PutBlock[s, @etherHeader, SIZE[EtherHeader]];
[] ← Streams.PutBlock[s, data.output, SIZE[StringBody[data.output.maxlength]]];
THROUGH [
SIZE[EtherHeader]+
SIZE[StringBody[data.output.maxlength]]..wordsPerPage)
DO
Streams.PutWord[s, 0];
ENDLOOP;
};
MesaToBCPLTime:
PROC [t: Time.Packed]
RETURNS [BCPLTime] =
INLINE
{RETURN [[high: Inline.HighHalf[t], low: Inline.LowHalf[t]]]};
MBTTY.PutString[tty, "Writing ether "L];
MBTTY.PutString[tty, IF data.germ THEN "germ"L ELSE "boot"L];
MBTTY.PutString[tty, "file..."L];
String.AppendString[name, data.output];
inFile ← Segments.NewFile[name, Segments.Read];
name.length ← 0; String.AppendString[name, data.etherOutput];
outFile ← Segments.NewFile[name, Segments.Write];
{
ENABLE
UNWIND => {
IF inStream ~= NIL THEN Streams.Destroy[inStream] ELSE Segments.ReleaseFile[inFile];
IF outStream ~= NIL THEN Streams.Destroy[outStream] ELSE Segments.ReleaseFile[outFile];
MBStorage.FreePages[buffer];
};
Segments.SetFileLength[outFile, Segments.GetFileLength[inFile] + bytesPerPage];
inStream ← Streams.CreateStream[inFile, Streams.Read];
outStream ← Streams.CreateStream[outFile, Streams.Write];
PutEtherHeader[outStream];
DO
words: CARDINAL = Streams.GetBlock[inStream, buffer, bufferPages*wordsPerPage];
IF words = 0 THEN EXIT;
IF Streams.PutBlock[outStream, buffer, words] ~= words THEN ERROR;
ENDLOOP;
};
Streams.Destroy[inStream];
Streams.Destroy[outStream];
MBStorage.FreePages[buffer];
MBTTY.PutLine[tty, "finished writing."L];
};
END.