// DiskStreamsScan.bcpl -- fast file scanner -- adjunct to DiskStreams
// Copyright Xerox Corporation 1979
// Last modified July 13, 1979  3:20 PM by Taft

get "DiskStreams.decl"

external
[
// outgoing procedures
InitScanStream; GetScanStreamBuffer; FinishScanStream

// incoming procedures
PositionPtr; CleanupDiskStream; SetLengthHint
VirtualDiskDA
InitializeDiskCBZ; DoDiskCommand; GetDiskCb
Allocate; Free; MoveBlock; Noop
]


//---------------------------------------------------------------------------
structure SSDx:  // Scan Stream Descriptor, extended version
//---------------------------------------------------------------------------
[
@SSD		// Public cells (da, pageNumber, numChars)
bd word		// -> current BD being worked on
nextPage word	// page number for next command to initiate; -1 => restart
thisCb word	// CB for this BD's next command
nextCb word	// CB for next command to chain to
ks word		// -> disk stream
cbz word	// -> CBZ
lenCBZ word	// length of CBZ in words
]
manifest lSSD = size SSDx/16

//---------------------------------------------------------------------------
structure BD:	// Buffer Descriptor
//---------------------------------------------------------------------------
[
nextBD word	// -> next BD in ring
full word	// false if empty or being filled, true if full
buffer word	// -> buffer for this BD
]
manifest lBD = size BD/16

//---------------------------------------------------------------------------
let InitScanStream(ks, bufTable, nBufs) = valof
//---------------------------------------------------------------------------
// Creates a Scan Stream Descriptor (SSD) in preparation for scanning
// the file corresponding to the stream ks.  bufTable is a table of pointers
// to page-size buffers, and nBufs is the number of buffers (there must
// be at least one).
// The SSD is allocated from the zone from which ks was allocated.
// Subsequent calls to GetScanStreamBuffer return pointers to buffers
// containing successive pages of the file, starting with the page at which
// the stream was positioned initially.
// No other operations on the stream should be performed while the scan
// is in progress (i.e., before FinishScanStream is called).
// Note: the da, pageNumber, and numChars cells in the SSD are public
// and refer to the page most recently returned by GetScanStreamBuffer.
[
// Get the stream into a clean state
PositionPtr(ks, 0)
CleanupDiskStream(ks)

// Initialize the SSD and the CBZ
let lenCBZ = ks>>KS.disk>>DSK.lengthCBZ + ks>>KS.disk>>DSK.lengthCB*(nBufs+2)
let ssd = Allocate(ks>>KS.zone, lSSD + lenCBZ + (nBufs+1)*lBD)
ssd>>SSDx.ks = ks
ssd>>SSDx.cbz = ssd+lSSD
ssd>>SSDx.lenCBZ = lenCBZ
ssd>>SSDx.nextPage = -1

// Initialize the BDs for the buffers we have been given
let bd = ssd>>SSDx.cbz + lenCBZ
ssd>>SSDx.bd = bd
for i = 0 to nBufs-1 do
   [
   bd>>BD.nextBD = bd+lBD
   bd>>BD.buffer = bufTable!i
   bd>>BD.full = false
   bd = bd>>BD.nextBD
   ]

// Initialize an extra BD for the stream buffer and mark it full
bd>>BD.nextBD = ssd>>SSDx.bd  // Close the ring
bd>>BD.buffer = ks>>KS.bufferAddress
bd>>BD.full = true

resultis ssd
]

//---------------------------------------------------------------------------
and GetScanStreamBuffer(ssd) = valof
//---------------------------------------------------------------------------
// Returns a pointer to a buffer containing the next page of the file
// being scanned, or zero if end-of-file has been reached.
// This pointer remains valid only until the next call on GetScanStreamBuffer.
[
ssd>>SSDx.bd>>BD.full = false
let nextDA = fillInDA
let ks, cbz = ssd>>SSDx.ks, ssd>>SSDx.cbz
let disk = ks>>KS.disk

// -1 means that this is the first call since the ssd was initialized,
// or that the length hint failed and we have to restart the transfer.
if ssd>>SSDx.nextPage eq -1 then
   [
   InitializeDiskCBZ(disk, cbz, ks>>KS.pageNumber+1,
    ssd>>SSDx.lenCBZ, Sretry, lv ks>>KS.bfsErrorRtn)
   cbz>>CBZ.cleanupRoutine = ScanCleanupCb
   cbz>>CBZ.client = ssd  // so the cleanup routine can find it

Sretry:  // Errors cause BfsGetCb to return here
   ssd>>SSDx.nextPage = cbz>>CBZ.currentPage
   nextDA = ks>>KS.DAs.next
   ssd>>SSDx.thisCb = GetDiskCb(disk, cbz)
   ssd>>SSDx.nextCb = GetDiskCb(disk, cbz)  // always keep one cb ahead
   ]

// The current BD is empty and is ready to have a new command initiated
// for it.  The next BD is either empty (because no command for it has
// ever been issued) or is in the process of being read into and has not
// yet been cleaned up.  In the latter case, the call to GetCb at the
// bottom of the loop will cause it to be cleaned up and marked full.
// This depends on the number of CBs being equal to the number of BDs plus one
// (the +1 is because we always keep one CB in our pocket).
   [ // repeat
   if ks>>KS.DAs.next eq eofDA then
      [
      unless nextDA eq eofDA resultis 0  // reached end-of-file
      // The following can happen only if the stream was already positioned
      // at end-of-file when the SSD was initialized.  Simply advance to
      // the BD with the stream buffer, without queueing new commands.
      ssd>>SSDx.bd = ssd>>SSDx.bd>>BD.nextBD; loop
      ]

   // Stop queueing commands if we have reached the alleged end-of-file
   if ssd>>SSDx.nextPage ne ks>>KS.hintLastPageFa.pageNumber+1 then
      [ // Queue new command using empty BD and chaining to nextCb
      DoDiskCommand(disk, ssd>>SSDx.thisCb, ssd>>SSDx.bd>>BD.buffer,
       nextDA, lv ks>>KS.fp, ssd>>SSDx.nextPage, DCreadD, ssd>>SSDx.nextCb)
      nextDA = fillInDA
      ssd>>SSDx.nextPage = ssd>>SSDx.nextPage+1
      ]

   // Advance BD, and get another CB to which the next command will be chained
   ssd>>SSDx.bd = ssd>>SSDx.bd>>BD.nextBD
   ssd>>SSDx.thisCb = ssd>>SSDx.nextCb
   ssd>>SSDx.nextCb = GetDiskCb(disk, cbz)
   ] repeatuntil ssd>>SSDx.bd>>BD.full

// If this is the page claimed by the length hint to be the last page
// of the file, and it isn't, then invalidate the hint and force the
// next call on GetScanStreamBuffer to restart the transfer.
if ks>>KS.pageNumber eq ks>>KS.hintLastPageFa.pageNumber &
 ks>>KS.DAs.next ne eofDA then
   [ ks>>KS.hintLastPageFa.pageNumber = 0; ssd>>SSDx.nextPage = -1 ]

ssd>>SSDx.da = ks>>KS.DAs.current
ssd>>SSDx.pageNumber = ks>>KS.pageNumber
ssd>>SSDx.numChars = ks>>KS.numChars
resultis ssd>>SSDx.bd>>BD.buffer
]

//---------------------------------------------------------------------------
and ScanCleanupCb(disk, cb, cbz) be
//---------------------------------------------------------------------------
// The cleanupRoutine called by BfsGetCb immediately before handing back
// a CB corresponding to a completed transfer.
[
let ssd = cbz>>CBZ.client
ssd>>SSDx.bd>>BD.full = true
let ks = ssd>>SSDx.ks
ks>>KS.DAs.last = ks>>KS.DAs.current
ks>>KS.DAs.current = ks>>KS.DAs.next
ks>>KS.DAs.next = cbz>>CBZ.nextDA
ks>>KS.pageNumber = ks>>KS.pageNumber+1
ks>>KS.numChars = cbz>>CBZ.currentNumChars
]

//---------------------------------------------------------------------------
and FinishScanStream(ssd) be
//---------------------------------------------------------------------------
// Waits for disk activity to cease, updates the state in the disk stream,
// and destroys the SSD.  The stream is left positioned at the beginning
// of the last page returned by GetScanStreamBuffer.
[
// Wait til all queued transfers have completed, but don't clean them up
// and don't bother to retry any that suffered errors.
let ks, cbz = ssd>>SSDx.ks, ssd>>SSDx.cbz
cbz>>CBZ.cleanupRoutine = Noop
cbz>>CBZ.retry = FlushRetry
FlushRetry: while cbz>>CBZ.head ne 0 do GetDiskCb(ks>>KS.disk, cbz)

// Put the contents of the current page into the stream buffer,
// if it's not already there.
MoveBlock(ks>>KS.bufferAddress, ssd>>SSDx.bd>>BD.buffer,
 ks>>KS.charsPerPage rshift 1)

// If we reached end-of-file, ensure that the length hint is correct.
// This is a no-op if we did not reach end-of-file.
SetLengthHint(ks)

Free(ks>>KS.zone, ssd)
]