FilePagesImpl.mesa - per-file operations, locking file header data structure, file page ops
Copyright © 1985 by Xerox Corporation. All rights reserved.
Andrew Birrell December 8, 1983 9:51 am
Levin, August 8, 1983 5:57 pm
Schroeder, June 10, 1983 5:20 pm
Bob Hagmann, January 9, 1986 4:01:58 pm PST
Russ Atkinson (RRA) May 15, 1985 8:29:35 pm PDT
DIRECTORY
Disk USING[ Channel, invalid, Label, ok, PageCount, PageNumber, Request, Status ],
DiskFace USING[ AbsID, DontCare, RelID, Tries, wordsPerPage ],
File USING[ Error, FP, GetVolumeID, PageCount, PageNumber, RC, Volume, VolumeID ],
FileBackdoor USING [],
FileInternal,
VolumeFormat USING[ AbsID, Attributes, lastLogicalRun, LogicalPage, LogicalPageCount, LogicalRun, LogicalRunObject, RelID, RunPageCount ],
VM USING[AddressForPageNumber, Allocate, Free, Interval, PageCount, PageNumber, PageNumberForAddress, PagesForWords, SwapIn],
VMBacking USING[RunTableIndex, RunTableObject, RunTablePageNumber];
FilePagesImpl: CEDAR MONITOR LOCKS FileInternal.FileImplMonitorLock
IMPORTS File, FileInternal, VM
EXPORTS DiskFace, File, FileBackdoor, FileInternal
SHARES File = {
Four different Impls all export some of these three data types. All exports are the same.
--DiskFace.--Attributes: PUBLIC TYPE = VolumeFormat.Attributes;
--DiskFace.--AbsID: PUBLIC TYPE = VolumeFormat.AbsID;
--DiskFace.--RelID: PUBLIC TYPE = VolumeFormat.RelID;
Handle: TYPE = REF Object;
--File.--Object: PUBLIC TYPE = FileInternal.Object;
RunTable: TYPE = FileInternal.RunTable;
RunTableObject: TYPE = VMBacking.RunTableObject;
RunTableIndex: TYPE = VMBacking.RunTableIndex;
PhysicalRun: TYPE = FileInternal.PhysicalRun;
lastRun: VMBacking.RunTablePageNumber = LAST[INT]; -- end marker in runTable --
MaxTransferRun: PUBLIC Disk.PageCount ← 200;
Largest number of disk pages to transfer in single request. This much VM will be pinned during the transfer. Variable, not constant, to allow patching.
scratchWriter: PUBLIC LONG POINTER; -- scratch buffer for writing (all 0)
scratchReader: PUBLIC LONG POINTER; -- scratch buffer for reading
badData: Disk.Status = [unchanged[dataCRCError]]; -- indicates data is bad but label is ok
labelTries: DiskFace.Tries = 5;
notReallyFree: PUBLIC INT ← 0;
FileInternal
******** Subroutines for access to file pages ******** --
HeaderLabel: PUBLIC PROC[fp: File.FP] RETURNS[Disk.Label] = {
FileInternal
RETURN[ [
fileID: [rel[RelID[fp]]],
filePage: 0,
attributes: Attributes[header],
dontCare: LOOPHOLE[LONG[0]]
] ]
};
DataLabel: PUBLIC PROC[fp: File.FP] RETURNS[Disk.Label] = {
FileInternal
RETURN[ [
fileID: [rel[RelID[fp]]],
filePage: 0,
attributes: Attributes[data],
dontCare: LOOPHOLE[LONG[0]]
] ]
};
FreeLabel: PUBLIC PROC[volume: File.Volume] RETURNS[Disk.Label] = {
FileInternal
RETURN[ [
fileID: [abs[AbsID[File.GetVolumeID[volume]]]],
filePage: 0,
attributes: Attributes[freePage],
dontCare: LOOPHOLE[LONG[0]]
] ]
};
WriteLabels: PUBLIC PROC[channel: Disk.Channel, diskPage: Disk.PageNumber, count: Disk.PageCount, data: LONG POINTER, label: POINTER TO Disk.Label] RETURNS[ status: Disk.Status, countDone: Disk.PageCount] = TRUSTED {
FileInternal
req: Disk.Request ← [
diskPage: diskPage,
data: IF data = NIL THEN scratchWriter ELSE data,
incrementDataPtr: data # NIL,
command: [header: verify, label: write, data: write],
count: count ];
[status, countDone] ← FileInternal.DoPinnedIO[channel, label, @req];
};
VerifyLabels: PUBLIC PROC[channel: Disk.Channel, diskPage: Disk.PageNumber, count: Disk.PageCount, label: POINTER TO Disk.Label] RETURNS[ status: Disk.Status, countDone: Disk.PageCount] = TRUSTED {
FileInternal
req: Disk.Request ← [
diskPage: diskPage,
data: FileInternal.scratchReader,
incrementDataPtr: FALSE,
command: [header: verify, label: verify, data: read],
count: count,
tries: labelTries ];
[status, countDone] ← FileInternal.DoPinnedIO[channel, label, @req];
IF status = badData
THEN { status ← Disk.ok; countDone ← countDone+1; label.filePage ← label.filePage+1 };
};
GetScratchPage: PUBLIC PROC RETURNS [data: LONG POINTER] = TRUSTED {
FileInternal
temp: LONG POINTER TO ARRAY [0..DiskFace.wordsPerPage) OF WORD;
interval: VM.Interval ← VM.Allocate[VM.PagesForWords[DiskFace.wordsPerPage]];
VM.SwapIn[interval];
data ← VM.AddressForPageNumber[interval.page];
temp ← LOOPHOLE[data];
temp^ ← ALL[0];
};
FreeScratchPage: PUBLIC PROC[data: LONG POINTER] = TRUSTED {
FileInternal
VM.Free[
[ page: VM.PageNumberForAddress[data],
count: VM.PagesForWords[DiskFace.wordsPerPage] ]
];
};
AddRun: PUBLIC PROC[file: Handle, run: POINTER TO PhysicalRun, logicalPage: VolumeFormat.LogicalPage, okPages: VolumeFormat.RunPageCount] = TRUSTED {
FileInternal
Either extend last run, or add new run
logical: LONG POINTER TO VolumeFormat.LogicalRunObject ← file.logicalRunTable;
physical: RunTable = file.runTable;
oldNRuns: CARDINAL ← physical.nRuns;
physical.nDataPages ← file.size + okPages;
IF oldNRuns > 0
AND logical[oldNRuns-1].first + logical[oldNRuns-1].size = logicalPage
AND logical[oldNRuns-1].size <= LAST[VolumeFormat.RunPageCount] - okPages
AND run.channel = physical[oldNRuns-1].channel
THEN logical[oldNRuns-1].size ← logical[oldNRuns-1].size + okPages
ELSE {
Add another run
IF physical.nRuns+1 = logical.maxRuns THEN {
Extend logical run table to ensure there is space to record an extension.
ExtendPhysicalRunTable[file];
logical ← file.logicalRunTable; -- recompute since it is changed by ExtendFileHeader in ExtendPhysicalRunTable
oldNRuns ← physical.nRuns;
};
physical[oldNRuns] ← run^;
logical[oldNRuns].first ← logicalPage;
logical[oldNRuns].size ← okPages;
physical.nRuns ← oldNRuns + 1;
IF physical.nRuns = physical.length THEN {
extend physical run table object
file.runTable ← NEW[RunTableObject[physical.length*2]];
file.runTable^ ← physical^ . . . . but the compiler can't copy sequences
file.runTable.nDataPages ← physical.nDataPages;
file.runTable.nRuns ← physical.nRuns;
FOR i: CARDINAL IN [0..physical.length) DO file.runTable[i] ← physical[i] ENDLOOP;
};
file.runTable[oldNRuns + 1].filePage ← lastRun;
logical[oldNRuns + 1].first ← VolumeFormat.lastLogicalRun;
};
};
ExtendPhysicalRunTable: PROC [file: Handle] = TRUSTED {
Extend logical run table to ensure there is space to record an extension.
Do this by allocating a single run that has enough pages for all header pages except page 0. Then write the labels and header data for the new header pages 1 on up. Write header page 0. If the write succeeds, we have committed to the new header pages. If we crash, we loose the new header pages (should be free but are not), but we have a consistent view of the header: the old one. Free old header pages.
newRunPages: VolumeFormat.LogicalPageCount =
FileInternal.RunsToHeaderPages[file.logicalRunTable.maxRuns] + 1;
diskPage1: Disk.PageNumber;
restOfHeaderSize: Disk.PageCount;
oldChannel: Disk.Channel;
[diskPage: diskPage1, size: restOfHeaderSize, channel: oldChannel] ← FileInternal.FindRun[
start: [-file.logicalRunTable.headerPages+1],
nPages: file.diskRunPages + file.diskPropertyPages - 1,
runTable: file.runTable];
IF restOfHeaderSize # file.diskRunPages + file.diskPropertyPages - 1 THEN
insist that headers 1 on up all are in the same run
ERROR File.Error[fragmented];
FileInternal.ExtendFileHeader[file: file, newRunPages: newRunPages, newPropertyPages: file.diskPropertyPages]; -- recomputes logical.maxRuns
};
RemoveFromRunTable: PUBLIC PROC[file: Handle, remove: INT] = TRUSTED {
FileInternal
logical: LONG POINTER TO VolumeFormat.LogicalRunObject = file.logicalRunTable;
physical: RunTable = file.runTable;
physical.nDataPages ← file.size;
WHILE remove > 0 DO
IF physical.nRuns = 0 THEN ERROR File.Error[inconsistent];
{
runSize: VolumeFormat.RunPageCount = logical[physical.nRuns-1].size;
amount: VolumeFormat.RunPageCount = MIN[remove, runSize];
logical[physical.nRuns-1].size ← runSize - amount;
IF runSize - amount = 0 THEN {
Remove the new run table entry entirely! --
physical.nRuns ← physical.nRuns - 1;
physical[physical.nRuns].filePage ← lastRun;
logical[physical.nRuns].first ← VolumeFormat.lastLogicalRun;
};
remove ← remove - amount;
};
ENDLOOP;
};
SpliceOutDataPage: PUBLIC PROC[file: Handle, filePage: File.PageCount] RETURNS [oldPage: Disk.PageNumber ← [0], oldChannel: Disk.Channel ← NIL] = TRUSTED {
FileBackdoor
runNumber: CARDINAL;
nearTo: VolumeFormat.LogicalPage;
newRun: FileInternal.PhysicalRun;
newLogicalRun: VolumeFormat.LogicalRun ;
labelsThisTime: Disk.PageCount ← 0;
IF filePage >= file.runTable.nDataPages THEN ERROR File.Error[unknownPage];
[diskPage: oldPage, channel: oldChannel] ← FindRun[start: [filePage], nPages: 1, runTable: file.runTable];
IF file.runTable.nRuns+3 >= file.logicalRunTable.maxRuns THEN { -- may split too soon
ExtendPhysicalRunTable[file]; -- make sure there is enough room
FileInternal.WriteRunTable[file]; -- write it out
};
[runNumber] ← SplitPhysicalRunTable[file: file, page: [filePage+file.runPages+file.propertyPages]];
IF filePage+1 < file.runTable.nDataPages THEN [] ← SplitPhysicalRunTable[file: file, page: [filePage+file.runPages+file.propertyPages+1]];
IF file.logicalRunTable.runs[runNumber].size # 1 THEN ERROR;
nearTo ← file.logicalRunTable.runs[runNumber].first;
WHILE labelsThisTime # 1 DO
need to loop because we may find pages free in VAM that are not really free
status: Disk.Status;
labelsOK: Disk.PageCount;
freeLabel: Disk.Label ← FileInternal.FreeLabel[file.volume];
newLogicalRun ← FileInternal.Alloc[volume: file.volume, first: nearTo, size: 1, min: 1 ];
freeLabel.filePage ← newLogicalRun.first;
[newRun.channel, newRun.diskPage] ← FileInternal.TranslateLogicalRun[newLogicalRun, file.volume];
nearTo ← [newLogicalRun.first + 1]; -- hint for next call of Alloc, if needed
TRUSTED{ [status, labelsOK] ←
FileInternal.VerifyLabels[newRun.channel, newRun.diskPage, newLogicalRun.size, @freeLabel] };
IF status # Disk.ok THEN notReallyFree ← notReallyFree+1; -- statistics
IF labelsOK = 1 THEN {
Suspected free page was free; write labels and data for header area
dataLabel: Disk.Label ← FileInternal.DataLabel[file.fp];
dataLabel.filePage ← filePage;
TRUSTED{[status, labelsThisTime] ← FileInternal.WriteLabels[newRun.channel,
newRun.diskPage, 1, file.headerVM+DiskFace.wordsPerPage, @dataLabel]};
};
ENDLOOP; -- end of WHILE labelsThisTime # 1 DO
file.logicalRunTable.runs[runNumber].first ← newLogicalRun.first;
FileInternal.WriteRunTable[file];
[] ← FileInternal.TranslateLogicalRunTable[file: file];
retranslate since we have extended and changed everything
};
SplitPhysicalRunTable: PUBLIC PROC[file: Handle, page: VolumeFormat.LogicalPage] RETURNS [runNumber: CARDINAL] = TRUSTED {
FileInternal == page is logical page in file with header page 0 as page 0.
logicalRunTable: LONG POINTER TO VolumeFormat.LogicalRunObject = file.logicalRunTable;
filePage: VolumeFormat.LogicalPage ← [0];
FOR runNumber IN [0..logicalRunTable.maxRuns) DO
IF logicalRunTable.runs[runNumber].first = VolumeFormat.lastLogicalRun THEN ERROR File.Error[inconsistent];
IF page = filePage THEN EXIT; -- split already in place
IF page >= filePage AND page < filePage+logicalRunTable.runs[runNumber].size THEN {
scratchLogicalRun: VolumeFormat.LogicalRun ← logicalRunTable.runs[runNumber];
copy forward all entries at and after the point of insertion
FOR j: CARDINAL IN [runNumber+1..logicalRunTable.maxRuns) DO -- asumes enough room in the run table
sLogicalRun: VolumeFormat.LogicalRun = logicalRunTable.runs[j];
logicalRunTable.runs[j] ← scratchLogicalRun;
IF scratchLogicalRun.first = VolumeFormat.lastLogicalRun THEN EXIT;
scratchLogicalRun ← sLogicalRun;
ENDLOOP;
logicalRunTable.runs[runNumber].size ← page - filePage;
runNumber ← runNumber+1;
logicalRunTable.runs[runNumber].first ← [logicalRunTable.runs[runNumber].first + page - filePage];
logicalRunTable.runs[runNumber].size ← logicalRunTable.runs[runNumber].size - page + filePage;
RETURN;
};
filePage ← [filePage+logicalRunTable.runs[runNumber].size];
ENDLOOP;
};
FreeRun: PUBLIC PROC[logicalRun: VolumeFormat.LogicalRun, volume: File.Volume, verifyLabel: POINTER TO Disk.Label ← NIL] = {
FileInternal
Write page labels as "free" and mark as unused in VAM. Iff verifyLabel # NIL, do so only to pages whose labels verify correctly.
label: Disk.Label ← FreeLabel[volume];
WHILE logicalRun.size > 0 DO
channel: Disk.Channel;
diskPage: Disk.PageNumber;
status: Disk.Status;
verifyStatus: Disk.Status ← Disk.ok;
thisTime: Disk.PageCount ← logicalRun.size;
countDone: Disk.PageCount;
Consume: PROC = {
logicalRun.first ← [logicalRun.first + countDone];
logicalRun.size ← logicalRun.size - countDone;
IF verifyLabel # NIL THEN TRUSTED {
verifyLabel.filePage ← verifyLabel.filePage + countDone;
};
countDone ← 0;
};
[channel, diskPage] ← FileInternal.TranslateLogicalRun[logicalRun, volume];
IF verifyLabel # NIL THEN TRUSTED {
verify which pages are part of our file
temp: Disk.Label ← verifyLabel^;
[verifyStatus, thisTime] ← VerifyLabels[channel, diskPage, thisTime, @temp];
};
label.filePage ← logicalRun.first;
FileInternal.Free[volume, [first: logicalRun.first, size: thisTime]];
TRUSTED{[status, countDone] ← WriteLabels[channel, diskPage, thisTime, NIL, @label]};
SELECT status FROM
Disk.ok => NULL;
Disk.invalid => ERROR File.Error[wentOffline, diskPage+countDone];
ENDCASE => countDone ← countDone + 1; -- Page is in our file, but not writeable
Consume[];
IF verifyLabel # NIL AND status = Disk.ok AND verifyStatus # Disk.ok THEN TRUSTED {
We've freed everything up to but excluding a label mis-match. Now, look for free pages which should be notified to the VAM in case the VAM thinks they're in use.
free: Disk.Label ← FreeLabel[volume];
free.filePage ← logicalRun.first;
[channel, diskPage] ← FileInternal.TranslateLogicalRun[logicalRun, volume];
[verifyStatus, countDone] ← VerifyLabels[channel, diskPage, logicalRun.size, @free];
IF verifyStatus # Disk.ok AND countDone = 0
THEN countDone ← 1 -- label isn't in our file, but isn't free, so ignore it
ELSE FileInternal.Free[volume, [first: logicalRun.first, size: countDone]];
Consume[];
}
ENDLOOP;
};
FindRun: PUBLIC PROC[start: File.PageNumber, nPages: File.PageCount, runTable: RunTable] RETURNS [diskPage: Disk.PageNumber, size: Disk.PageCount, channel: Disk.Channel] = {
FileInternal
probe: RunTableIndex ← runTable.nRuns / 2; -- NB: round down --
increment: CARDINAL ← probe;
IF runTable.nRuns = 0 THEN ERROR File.Error[inconsistent];
IF start + nPages > runTable.nDataPages THEN ERROR File.Error[unknownPage, start];
IF start < runTable[0].filePage THEN ERROR File.Error[unknownPage, start];
DO increment ← (increment+1)/2;
SELECT TRUE FROM
runTable[probe].filePage > start =>
probe ← IF probe < increment THEN 0 ELSE probe-increment;
runTable[probe+1].filePage <= start =>
probe ← MIN[probe + increment, runTable.nRuns-1];
ENDCASE =>
RETURN[
diskPage: [runTable[probe].diskPage + (start-runTable[probe].filePage)],
size: IF start + nPages <= runTable[probe+1].filePage
THEN nPages
ELSE runTable[probe+1].filePage - start,
channel: runTable[probe].channel ]
ENDLOOP;
};
maxTransferRun: Disk.PageCount ← 200;
Largest number of disk pages to transfer in single request. This much VM will be pinned during the transfer. Variable, not constant, to allow patching.
Transfer: PUBLIC PROC[file: Handle, data: LONG POINTER, filePage: File.PageNumber, nPages: File.PageCount, action: FileInternal.ActionType, where: FileInternal.WhereLocation ] = {
FileInternal
label: Disk.Label ←
IF where = header THEN FileInternal.HeaderLabel[file.fp] ELSE FileInternal.DataLabel[file.fp];
label.filePage ← filePage;
WHILE nPages > 0 DO
status: Disk.Status;
countDone: Disk.PageCount;
channel: Disk.Channel;
req: Disk.Request;
firstDiskPage: Disk.PageNumber;
req.data ← data;
req.incrementDataPtr ← TRUE;
req.command ← IF action = read
THEN [header: verify, label: verify, data: read]
ELSE [header: verify, label: verify, data: write];
TRUSTED{ [diskPage: req.diskPage, size: req.count, channel: channel] ← FileInternal.FindRun[ -- NB: first call of FindRun checks entire transfer is within file
start: IF where = header THEN [-file.logicalRunTable.headerPages+filePage] ELSE filePage,
nPages: nPages,
runTable: file.runTable] };
firstDiskPage ← req.diskPage;
IF req.count > maxTransferRun THEN req.count ← maxTransferRun;
TRUSTED{[status, countDone] ← FileInternal.DoPinnedIO[channel, @label, @req]};
FileInternal.CheckStatus[status, firstDiskPage+countDone]; -- use firstDiskPage instead of req.diskPage since DiskImpl modifies req.diskPage
TRUSTED{data ← data + countDone * DiskFace.wordsPerPage};
nPages ← nPages - countDone;
filePage ← [filePage + countDone];
ENDLOOP;
};
WriteRunTable: PUBLIC PROC[file: Handle] = TRUSTED {
Exported to FileInternal
Caller guarantees header will not need to be expanded.
IF file.diskRunPages = file.runPages AND file.diskPropertyPages = file.propertyPages
THEN {
Normal case: one page run table and single property page, or header size on disk is correct. Page level atomicity guarantees file invariants.
FileInternal.Transfer[file: file, data: file.headerVM, filePage: [0], nPages: file.runPages, action: write, where: header];
}
ELSE {
Abnormal case: header is being extended. Be careful to build a new header pages 1 on up before writing page 0. It is possible to guarantee file invariants, but still loose some pages (headers written, not in disk run table) if we crash at the wrong time.
labelsThisTime: Disk.PageCount ← 0;
size: VolumeFormat.LogicalPageCount = file.runPages + file.propertyPages - 1 ;
nearTo: VolumeFormat.LogicalPage ← file.logicalRunTable.runs[0].first;
Get run table in acceptable format: run 0 is one page long, run 1 has the rest of the header(s), the rest of the runs are for data pages.
IF file.logicalRunTable.runs[0].size # 1 THEN {
[] ← FileInternal.SplitPhysicalRunTable[file: file, page: [1]];
};
IF file.logicalRunTable.runs[1].size # file.diskRunPages+file.diskPropertyPages-1 THEN {
[] ← FileInternal.SplitPhysicalRunTable[file: file, page: [file.diskRunPages+file.diskPropertyPages]];
};
WHILE labelsThisTime # size DO
need to loop because we demand run number two contain all the rest of the header
logicalRun: VolumeFormat.LogicalRun;
status: Disk.Status;
labelsOK: Disk.PageCount;
run: FileInternal.PhysicalRun;
freeLabel: Disk.Label ← FileInternal.FreeLabel[file.volume];
logicalRun ← FileInternal.Alloc[volume: file.volume, first: nearTo, size: size, min: size];
freeLabel.filePage ← logicalRun.first;
[run.channel, run.diskPage] ← FileInternal.TranslateLogicalRun[logicalRun, file.volume];
nearTo ← [logicalRun.first + size]; -- hint for next call of Alloc, if needed
TRUSTED {
[status, labelsOK] ← FileInternal.VerifyLabels[run.channel, run.diskPage, logicalRun.size, @freeLabel]
};
IF status # Disk.ok THEN notReallyFree ← notReallyFree+1; -- statistics
IF labelsOK = size THEN {
Suspected free pages were free; write labels and data for header area
headerLabel: Disk.Label ← FileInternal.HeaderLabel[file.fp];
headerLabel.filePage ← 1;
TRUSTED {
[status, labelsThisTime] ← FileInternal.WriteLabels[run.channel, run.diskPage, size, file.headerVM+DiskFace.wordsPerPage, @headerLabel];
};
IF labelsThisTime = size
THEN {
destroyLogicalRun: VolumeFormat.LogicalRun ← file.logicalRunTable.runs[1];
file.logicalRunTable.runs[1] ← logicalRun;
file.diskRunPages ← file.runPages ;
file.diskPropertyPages ← file.propertyPages ;
file.logicalRunTable.headerPages ← file.diskRunPages + file.diskPropertyPages ;
[] ← FileInternal.TranslateLogicalRunTable[file];
re-translate since we have changed everything
Go for commit on the new header pages.
FileInternal.Transfer[file: file, data: file.headerVM, filePage: [0], nPages: 1, action: write, where: header];
Free old header pages
headerLabel.filePage ← 1;
FileInternal.FreeRun[logicalRun: destroyLogicalRun, volume: file.volume, verifyLabel: @headerLabel];
}
ELSE {
Some pages would not write. Rewrite labels of pages written OK, and fix up VAM.
IF labelsThisTime > 0 THEN {
headerLabel.filePage ← 1;
FileInternal.FreeRun[
[first: [logicalRun.first], size: labelsOK], file.volume, @headerLabel];
};
IF size - labelsThisTime - 1 > 0 THEN {
deallocate pages where labels not changed
FileInternal.Free[volume: file.volume, logicalRun: [first: [logicalRun.first + labelsOK + 1], size: size - labelsThisTime - 1]];
};
};
}
ELSE {
Some labels were not free (verify failed) even though the VAM said they were free. Free pages with the labels changed, and fix up VAM for pages not changed.
IF labelsOK > 0 THEN
FileInternal.Free[volume: file.volume, logicalRun: [first: [logicalRun.first], size: labelsOK]];
IF size - labelsOK - 1 > 0 THEN
FileInternal.Free[volume: file.volume, logicalRun: [first: [logicalRun.first + labelsOK + 1], size: size - labelsOK - 1]];
};
ENDLOOP;
};
};
scratchWriter ← GetScratchPage[];
scratchReader ← GetScratchPage[];
}.
Bob Hagmann January 14, 1985 9:13:04 am PST
Split from FileImpl due to storage overflow in compiler
Bob Hagmann January 9, 1986 3:58:46 pm PST
Fixes to bad status reporting
changes to: Transfer