# The RegularFile interface

A `RegularFile.T`, or regular file handle, provides access to a persistent extensible sequence of bytes.

    INTERFACE RegularFile;

    IMPORT File, OSError;

    TYPE
      T <: Public;
      Public = File.T OBJECT METHODS
        seek(origin: Origin; offset: INTEGER): INTEGER
          RAISES {OSError.E};
        flush() RAISES {OSError.E};
        lock(): BOOLEAN RAISES {OSError.E};
        unlock() RAISES {OSError.E}
      END;
      Origin = {Beginning, Current, End};

    VAR (*CONST*) FileType: File.Type;

Equal to `Atom.FromText(*RegularFile*).`

    END RegularFile.

Like every `File.T`, a regular file handle `h` has the components

     type(h)      an atom, equal to "FileType"
     readable(h)  a boolean
     writable(h)  a boolean

A regular file handle `h` also has the components

     cur(h)       an integer, the index of the next byte to read or write
     file(h)      the identity of a regular file

There may be distinct regular file handles `h1` and `h2` with `file(h1)` equal to `file(h2)`, and more than one process may hold a single regular file handle (see `Process.Create`).

A regular file (not a handle) `f` has the components

     buffer(f)  an extensible byte sequence
     stable(f)  an extensible byte sequence
     mtime(f)   a "Time.T", the last modification time
     locked(f)  a "Process.ID"

The sequences `buffer(f)` and `stable(f)` are zero-based and always have the same length. `stable(f)` represents the contents of the file on the disk or other persistent storage medium, while `buffer(f)` represents write-behind caching performed by the operating system. From time to time, a daemon performs

     WITH i = some integer "i" in the range "[0..len(buffer(f))-1]" DO
       stable(f)[i] := buffer(f)[i]
     END

The methods described in this interface are atomic with respect to the daemon.

The meaning of the call

     h.read(b, mayBlock)

is given by the specification of `File.T.read` together with these definitions, where `f = file(h)`:

     src(h)    = buffer(f)
     srcCur(h) = cur(h)
     srcEof(h) = TRUE

Because `srcEof(h)` is always `TRUE`, `read` never blocks. However, a subsequent read can return more data if an interleaved write extends `buffer(f)`. If `cur(h)` is negative (because of a prior seek), `read` raises `OSError.E`.

The meaning of the call

     h.write(b)

is given by the specification of `File.T.write` together with these definitions, where `f = file(h)`:

     snk(h)    = buffer(f)
     snkCur(h) = cur(h)

In addition, `write` sets `mtime(file(h))` to the current time. If `write` is called when `cur(h) > size(f)` (because of a prior seek), it extends `f` with bytes of undefined value. If `cur(h)` is negative, `write` raises `OSError.E`.

The call

     h.status(stat)

is equivalent to the following, in which `stat` is a local variable of type `Status`:

     stat.type := FileType;
     stat.modificationTime := mtime(file(h));
     stat.size := len(buffer(file(h)));
     RETURN stat

The call

     h.seek(origin, offset)

is equivalent to

     CASE origin OF
       Origin.Beginning => cur(h) := offset
     | Origin.Current => cur(h) := cur(h)+offset
     | Origin.End => cur(h) := len(buffer(file(h)))+offset
     END;
     RETURN cur(h)

Note that `seek` never changes the length of the file, although a subsequent write may do so. Use the call `h.seek(Origin.Current, 0)` to determine `cur(h)` without changing it.

The call

     h.flush()

is equivalent to

     WITH f = file(h) DO
       FOR i := 0 TO len(buffer(f))-1 DO
         stable(f)[i] := buffer(f)[i]
       END
     END

The call

     h.close()

extends the normal action of the `close` method with

     IF locked(file(h) = Process.GetMyID() THEN
       locked(file(h)) := Process.NullID
     END

If the file h is not already locked by the calling process (i.e., if locked(file(h)) # Process.GetMyID()), the call

     h.lock()

is equivalent to:

     IF locked(file(h)) = Process.NullID THEN
       locked(file(h)) := Process.GetMyID();
       RETURN TRUE
     ELSIF locked(file(h)) = Process.GetMyID() THEN
       RETURN TRUE
     END;
     RETURN FALSE

In the event that h is already locked by the calling process, the result of h.lock() is implementation-dependent. However, clients can work around the undefined nature of the operation in this case by keeping track of locked(file(h)) explicitly.

The call

     h.unlock()

is equivalent to:

     IF locked(file(h)) # Process.GetMyID() THEN
       RAISE OSError.E
     END;
     locked(file(h)) := Process.NullID

Some implementations raise an exception if a process tries to read or write a file locked by another process. You should treat this as a checked runtime error rather than writing code to catch and recover from the exception; the same applies to unlocking a file that you didn't lock.

You lock a file with code like

     CONST
       MaxTry = 3;
       RetryInterval = 5.0D0;
     VAR try := 1;
     BEGIN
       WHILE NOT h.lock() DO
         IF try=MaxTry THEN Give up END;
         INC(try);
         Time.Pause(RetryInterval)
       END;
       TRY Read or write "h" FINALLY h.unlock() END
     END

The regular file underlying a regular file handle is monitored, thus allowing concurrent operations. We leave unspecified the unit of atomicity for reads and writes, so a set of processes sharing a file that needs to be updated should use the `lock` and `unlock` methods. A regular file handle itself should be treated as unmonitored. A client thread typically needs to perform a `seek` followed by a `read` or `write` as an atomic unit, which can be implemented with a mutex in the client.