INTERFACE IO; (***************************************************************************) (* Copyright (C) Olivetti 1989 *) (* All Rights reserved *) (* *) (* Use and copy of this software and preparation of derivative works based *) (* upon this software are permitted to any person, provided this same *) (* copyright notice and the following Olivetti warranty disclaimer are *) (* included in any copy of the software or any modification thereof or *) (* derivative work therefrom made by any person. *) (* *) (* This software is made available AS IS and Olivetti disclaims all *) (* warranties with respect to this software, whether expressed or implied *) (* under any law, including all implied warranties of merchantibility and *) (* fitness for any purpose. In no event shall Olivetti be liable for any *) (* damages whatsoever resulting from loss of use, data or profits or *) (* otherwise arising out of or in connection with the use or performance *) (* of this software. *) (***************************************************************************) IMPORT Thread, Text, Fmt, CharType; TYPE Stream <: Thread.Mutex; (* IO provides input/output facilites via a stream type. This interface provides operations on open streams. Routines which create and open streams are exported by separate interfaces. Each such interface is expected to open a stream to a specific class of source/destination e.g. 'FileStream' should provide a routine which opens a stream to a file. Given an open stream a user can read characters from it using get operations and write characters to it using put operations. These provide sequential access to the stream i.e. a get operation will get some number of characters from the stream and update the stream position so a subsequent get will read the next characters. A put operation puts some number of characters to the stream and updates the stream position so a subsequent put will write after those characters. Random access to a stream is provided by the tell and seek operations. The tell operation returns the current stream position; the seek operation allows the current stream position to be altered. The stream package distinguishes two different types of stream, mapped and unmapped. A mapped stream is a stream to, for example, a file where every byte written or read at a given position in the stream corresponds to, or "maps" the byte at that position in the file. Mapped streams have known length, can be seekable and may support both put and get operations. An unmapped stream is typically to or from some kind of device e.g. a keyboard. Such streams have unknown length, are not seekable and support only put or only get operations. The last restriction may seem a little harsh - it might be nice to have a stream that supports reading from the keyboard and writing to the screen for example. Unfortunately such a stream would have different semantics from a mapped stream which supports both puts and gets. For example consider doing a put operation on a bidirectional mapped stream. The put alters the stream position so if you, say, put 3 characters at the start of a file and then get a character you overwrite the first 3 characters of the file and get the 4th character. If you put 3 characters to the screen, however, you certainly don't want to "overwrite" the first 3 characters from the keyboard! So the keyboard/screen should be handled by two separate streams, one for input and one for output. The stream operations available are described in more detail below. Note that the stream operations do not lock the stream; locking and unlocking the stream for every operation would be too inefficient. The user is expected to surround a group of stream operations by a LOCK statement e.g. LOCK s DO ...lots of operations on 's'... END; *) TYPE OpenMode = {Read, Write, Append, Update, WriteAndRead, AppendAndRead}; (* A stream is opened with a given 'OpenMode'. The 'OpenMode' may restrict the operations which are allowed on a stream. A description of the modes follows. The description assumes that the source/destination of the stream is a file. This is because most people understand files and streams to files are very common. Following the description of the modes is a brief discussion of open modes when the source/destination is not a file. Read The stream is read only. The source file must already exist. The initial stream position is zero. Write The stream is write only. If the destination file already exists it is truncated to zero length, otherwise a new file is created. The initial stream position is zero. Append The stream is write only. Writes (i.e. put operations) can only be done at the end of the stream; an attempt to write when the stream position is not at the end of the stream will cause an error. If the destination file already exists it is used and the initial stream position is at the end of the file. If the destination file does not exist it is created and the initial stream position is zero. Update The stream is readable and writable. If the source/destination file exists it is used, otherwise a new file is created. The initial stream position is zero. WriteAndRead Exactly like Write mode except that the stream is readable as well as writable. AppendAndRead Exactly like Append mode except that the stream is readable as well as writable. When an interface exports a routine which opens streams to something which is not a file it may not allow some open modes. The aim should be to retain only those open modes which "make sense" for the source/destination of the stream. If the stream will be unmapped the mode should be one of 'Read' or 'Write'. For example suppose there is a routine which opens a stream to a printer. Such a routine should only allow the 'Write' open mode. Note too that the meaning of the 'Write' mode has been restricted - the part about truncating any existing file does not apply. In general restrictions introduced by properties of the source/destination are "common sense". They should, however, be documented by a comment in the appropriate interface *) EXCEPTION Error(Stream); TYPE Fault = {None, NotInitialized, NotReadable, NotWritable, NotSeekable, SeekOutOfRange, BadTruncate, BadAppend, BadUnget, Disabled, Closed, Open, Read, Write, Seek, Truncate, Close, ImplSpecific}; CONST ImplFaults = SET OF Fault{ Fault.Open, Fault.Read, Fault.Write, Fault.Seek, Fault.Truncate, Fault.Close, Fault.ImplSpecific}; (* When an error occurs on a stream the 'Error' exception is raised. The exception argument is the errant stream. The user can find out details of the error from the errant stream. Only a few operations are allowed on an errant stream. They are: 1) Enquiry functions which do not alter the stream state and are still meaningful after an error ('Tell', for example, is not allowed because the error may have affected the stream position in some undefined way). The full list of allowable enquiry functions is: 'Properties', 'Closed', 'WhyErrant', 'DescribeError', 'Name', But probably only 'WhyErrant', 'DescribeError' and 'Name' will be useful. 2) 'Clear' which attempts to recover from the error. 2) 'Close' which closes the stream. Calling any other operation on an errant stream will result in the original error being raised again. Errors fall into two broad classes 1) User errors - e.g. writing to a read only stream, calling 'Unget' without a previous call to 'Get'. 2) Stream failure - e.g. failure to write to a file because the disc is full. There are some errors which do not clearly fall into either class but the distinction is usually useful. The enumeration 'Fault' describes the various types of errors that can occur. The set 'ImplFaults' contains all the errors which fall into the "stream failure" class. Streams are intended to be efficient; in particular streams are often buffered and 'Get' and 'Put' are usually buffer operations which only do any heavy work if the buffer is exhausted or full. Making the stream as efficient as possible can lead to some compromise in error detection. The compromise allowed by this interface is that not all errors are detected immediately. e.g. a put which fills up a disc will not be detected until the stream buffer is flushed. Note that sometimes this may lead to an error being lost completely. Taking the example of the put which should fill up the disc. If the put is not flushed out and another error occurs or the buffer is discarded due to a 'Truncate' call the full disc will never actually happen! *) (* Stream properties *) TYPE Property = { Mapped, Readable, Writable, Seekable, Truncatable, AppendOnly, Unbuffered, LineBuffered, IsBuffer}; PropertySet = SET OF Property; CONST NoProperties = PropertySet{}; AllProperties = PropertySet{FIRST(Property)..LAST(Property)}; PROCEDURE Properties(s: Stream): PropertySet RAISES {}; (* Returns the properties of 's'. This operation does not check if 's' is errant, closed, disabled or uninitialized (though it will give a checked runtime error if 's' is NIL). The properties are: Mapped: Is the stream mapped? Readable, Writable, Seekable, Truncatable: Fairly obvious - describe the operations available on the stream. AppendOnly: An append only stream restricts put operations to the end of the stream. Unbuffered: Every put operation is flushed. LineBuffered: The stream is flushed whenever a newline character is put (it may be flushed more often than that, due to buffering). IsBuffer: A special stream where the source/destination of the stream is the buffer. Such a stream never needs to be flushed. *) (* Now the stream operations. It is a checked runtime error if the stream argument to a stream operation is NIL, unless stated otherwise. *) (* Writing to a stream *) <*INLINE*> PROCEDURE Put(s: Stream; char: CHAR) RAISES {Error}; (* Put a single character *) <*INLINE*> PROCEDURE PutN( s: Stream; READONLY chars: ARRAY OF CHAR) RAISES {Error}; (* Put an array of characters. If 'NUMBER(chars) = 0' 'PutN' is a null operation and will succeed even if 's' is invalid or unusable *) PROCEDURE Flush(s: Stream) RAISES {Error}; (* If the stream is buffered 'Flush' ensures that the buffer is flushed. Note that this does not guarantee that the eventual destination of the stream is updated; it is possible, for example, that the underlying stream implementation has a further level of buffering *) (* Reading from a stream *) EXCEPTION EndOfStream(Stream); <*INLINE*> PROCEDURE Get(s: Stream): CHAR RAISES {Error,EndOfStream}; (* Get a single character. If there are no more characters 'EndOfStream' is raised with 's' as the exception argument *) PROCEDURE GetUntil( s: Stream; VAR chars: ARRAY OF CHAR; READONLY terminate := CharType.WhiteSpace; unget := TRUE) : CARDINAL RAISES {Error}; (* Gets characters from 's' and puts them into 'chars' until the end of stream is reached, a character which is in the set 'terminate' is read or 'chars' is filled up: a) If the end of stream is reached no exception is raised and the number of characters put into 'chars' is returned. b) If a terminating character is read it is not put into 'chars' and is ungot unless 'unget' is FALSE (i.e. if 'unget' is TRUE 'GetUntil' will ensure that an immediately following get/put operation will read/overwrite the terminator first). The number of characters put into 'chars' is returned. c) If 'chars' is filled up one more character is read. If end of stream is reached or the character is a terminator a) or b) applies. Otherwise the character is unread and NUMBER(chars) + 1 is returned to indicate overflow *) PROCEDURE Skip( s: Stream; READONLY skip := CharType.WhiteSpace; unget := TRUE) : CHAR RAISES {Error, EndOfStream}; (* Gets characters from 's' until the end of stream is reached or a character which is not in the set 'skip' is read. If a character which is not in the set 'skip' is read it is returned as the result and ungot if 'unget' is TRUE. If end of stream is reached the 'EndOfStream' exception is raised. *) <*INLINE*> PROCEDURE GetN( s: Stream; VAR chars: ARRAY OF CHAR; raiseEndOfStream := FALSE) : CARDINAL RAISES {Error, EndOfStream}; (* Fill 'chars' with characters read from 's'. Returns the number of characters read - hence the result will usually be 'NUMBER(chars)'. If the result is less than 'NUMBER(chars)' the end of stream has been reached. If 'raisesEndOfStream' is TRUE the result will always be 'NUMBER(chars)'; if end of stream is reached before 'chars' can be completely filled 'EndOfStream' is raised. If 'chars' is null i.e. 'NUMBER(chars) = 0', 'GetN' is a null operation and will always succeed *) PROCEDURE GotEndOfStream(s: Stream): BOOLEAN RAISES {Error}; (* Returns TRUE if the last get operation hit end of stream. Normally the 'EndOfStream' exception indicates this but 'GotEndOfStream' is useful after get routines which do not raise end of stream (e.g. 'GetN', 'GetUntil' and get routines built on top of 'Get' which catch 'EndOfStream' internally). *) (* Stream length *) CONST UnknownLength = LAST(CARDINAL); PROCEDURE Length(s: Stream): CARDINAL RAISES {Error}; (* Returns the length of 's'. If 's' is an unmapped stream the length of 's' is not known and 'UnknownLength' is returned *) PROCEDURE Truncate(s: Stream; length: CARDINAL) RAISES {Error}; (* set the length of 's' to be 'length'. 'length' must be less than the current length of 's'. 's' must truncatable (this can be checked by looking at the stream properties). If 'length' is less than the current stream position the current stream position is reset to 'length' *) (* Random access *) PROCEDURE Tell(s: Stream): CARDINAL RAISES {Error}; (* Returns the current position in the stream *) TYPE SeekMode = {Beginning, Current, End}; PROCEDURE Seek( s: Stream; offset := 0; mode: SeekMode := SeekMode.Beginning) RAISES {Error}; (* set the current position in the stream according to 'offset' and 'mode'. 's' must be a seekable stream. The different modes are: Beginning: In this mode 'offset' is an offset from the start of the file (and should be positive). Current: In this mode 'offset' is an offset from the current stream position End: In this mode 'offset' is an offset from the end of the stream (and should be negative). If the position computed from 'mode' and 'offset' is less than zero or greater than the length of the stream, or if the stream is not seekable, 'Error' is raised. *) (* Unget *) PROCEDURE Unget(s: Stream) RAISES {Error}; (* Allows a limited back tracking facility even on non seekable streams. The 'Unget' operation has the same effect as 'Seek(s, -1, SeekMode.Current)'. It is special because: 1) 'Unget' can be used even if the stream is not seekable. 2) It is only works if the previous operation on the stream was a call to 'Get', 'GetUntil' or 'Skip'. Furthermore these routines must not have raised 'Error' and the 'unget' argument to 'GetUntil' or 'Skip' must have been FALSE. If these conditions are not met 'Unget' will raise 'Error'. 3) If the preceding get operation raised 'EndOfStream', 'Unget' does nothing *) (* Error handling *) PROCEDURE WhyErrant(s: Stream): Fault RAISES {}; (* Returns a value indicating the type of the error which has occured on 's'. The possible values are listed below. None: No error has occured, the stream is not errant. NotInitialized: The stream is uninitialized. This is usually the fault of the stream implementor not the stream user; users should not be able to get their hands on uninitialized streams. NotReadable: A get operation has been used on a write only stream. NotWritable: A put operation has been used on a read only stream. NotSeekable: A seek operation has been used on a non seekable stream. SeekOutOfRange: A seek has been requested to a position which is either less than zero or greater than the known stream length. BadTruncate: Truncate has been used on a non truncatable stream BadAppend: A put operation has been used on an append only stream when the stream position was not at the end of the stream. BadUnget: An unget has been used without a preceding 'Get', 'GetUntil' or 'Skip' operation. Disabled: An attempt has been made to use a disabled stream. It is possible to build a layer on a stream to do fast, unsafe IO. This layer disables the underlying stream so that it is impossible to use it except via procedure provided by the layer. Closed: An attempt has been made to use a closed stream. Open: Failure to open a stream. Read: Failure to read from the stream source Write: Failure to write to the stream destination Seek: Failure to seek in the stream source/destination Truncate: Failure to truncate the stream destination Close: Failure to close the stream source/destination ImplSpecific: Failure of a stream class specific operation. Some interfaces may provide streams which support operations in addition to those provided by this interface. This error type is reserved for the failure of such operations. *) PROCEDURE FaultToText(f: Fault): Text.T RAISES {}; (* Returns a text describing the given fault. The text is brief and is not terminated with a newline. *) PROCEDURE Name(s: Stream): Text.T RAISES {}; (* Every stream is given a name when it is created. 'Name' returns the stream name, which may be useful in an error message. This routine does not check if 's' is uninitialized, disabled, closed or errant. It never returns NIL. *) PROCEDURE DescribeError(s: Stream): Text.T RAISES {}; (* Return a text describing in some detail the reason for the error on 's'. Returns NIL if no error has occured on 's' or if no further information is available. Note that if the error is a user error 'DescribeError' always returns NIL; the result of 'WhyErrant' is sufficient to describe such errors. The message should be brief and is not terminated with a newline. This routine does not check if 's' is uninitialized, disabled, closed or errant *) CONST PermanentFaults = SET OF Fault{Fault.Open, Fault.Disabled, Fault.Close}; PROCEDURE Clear(s: Stream): BOOLEAN RAISES {}; (* Attempts to recover 's' after an error. Returns TRUE if a successful recovery is made or if 's' is not errant. If 's' cannot be salvaged it is left errant and FALSE is returned. 'Clear' is guaranteed to work after a user error (i.e. if 'WhyErrant(s)' is not in 'ImplFaults'). It is guaranteed to fail if 'WhyErrant(s)' is in 'PermanentFaults'. Clear returns FALSE if 's' is closed or uninitialized. After a 'Clear' of a user error the stream will be as it was before the error. After a 'Clear' of an 'ImplError' the stream position and length may have changed and if there was data buffered at the time of the error it will have been lost. The stream is usable again but the user should check the position and length, if they are of interest. *) (* Close *) PROCEDURE Close(s: Stream; quiet: BOOLEAN := FALSE) RAISES {Error}; (* Closes 's'. If the stream is open for writing and is buffered the buffer is flushed before it is closed. If 's' has already been closed 'Close' will have no effect and no exception will be raised. If 's' is uninitialized or disabled 'Error' will be raised before any attempt is made to close the stream. If the 'quiet' argument is TRUE any error during the flushing and closing of 's' will be ignored (this does not affect the check to see if 's' is uninitialized or disabled). If the stream is successfully closed it is marked as closed. A closed stream behaves like an errant stream in that most operations are prohibited but it is even less useful than an errant stream because 'Clear' always fails if applied to a closed stream. *) PROCEDURE Closed(s: Stream): BOOLEAN RAISES {}; (* Is 's' closed?. Does not check if the stream is uninitialized, disabled or errant. It is a checked runtime error if 's' is NIL *) (* The following operations build on the basic put and get functions to provide more complex put and get operations. *) PROCEDURE PutText(s: Stream; t: Text.T) RAISES {Error}; (* Put a text. The 'Fmt' interface, together with the '&' operator, make any more complex put functions unnecessary. It is a checked runtime error if 't' is NIL. If 't' is the null text, i.e. has zero length, 'PutText' is a null operation which will always succeed. *) PROCEDURE PutF( s: Stream; fmt: Text.T; t1, t2, t3, t4, t5: Text.T := NIL) RAISES {Error}; (* Has the same effect as 'PutText(s, Fmt.F(fmt, t1, t2, t3, t4, t5))' *) PROCEDURE PutFN( s: Stream; fmt: Text.T; READONLY texts: ARRAY OF Text.T) RAISES {Error}; (* Has the same effect as 'PutText(s, Fmt.FN(fmt, texts))' *) <*INLINE*> PROCEDURE SkipUntil( s: Stream; READONLY terminate := CharType.EndOfLine; unget := FALSE) : CHAR RAISES {Error, EndOfStream}; (* A convenience function - just a veneer on 'Skip'. Gets characters from 's' until the end of stream is reached or a character which is in the set 'terminate' is read. If end of stream is reached the 'EndOfStream' exception is raised. If a character in 'terminate' is read it is returned as the result and ungot if 'unget' is TRUE. *) PROCEDURE GetChars( s: Stream; VAR chars: ARRAY OF CHAR; READONLY skip := CharType.WhiteSpace; READONLY terminate := CharType.WhiteSpace; unget := TRUE) : CARDINAL RAISES {Error, EndOfStream}; (* Reads characters from 's' and puts them into 'chars'; returns the number of characters inserted into 'chars' or 'NUMBER(chars) + 1' if 'chars' overflows. First uses 'Skip' to skip any characters in 'skip', then reads characters and puts them into 'chars' using 'GetUntil'. Note that 'EndOfStream' is only raised if the end of stream is reached during the initial skip. Once a significant character has been read the end of stream is treated as a terminator. 'GetChars' may return 0. This happens if it encounters a terminal character before it finds a character not in the 'skip' set *) PROCEDURE GetText( s: Stream; READONLY skip := CharType.None; READONLY terminate := CharType.WhiteSpace; unget := TRUE) : Text.T RAISES {Error, EndOfStream}; (* Similar to 'GetChars' but builds a text instead of putting characters into an array. There is no limit to the length of text which can be read (other than running out memory!) *) (* The following procedures read numbers or booleans from the given stream. They all use the same approach: Skip characters in the 'skip' set then read characters until a character in the 'terminate' set is read or the end of stream is reached. If 'unget' is TRUE the terminating character, if any, is unread. The significant characters read (i.e. those read after the skip but not including the terminator) are converted into a number/boolean. If no significant characters are read, the significant characters are not a valid number/boolean or if overflow occurs, 'Invalid' is raised. The argument to 'Invalid' is a text containing the significant characters. Note that 'EndOfStream' is only raised if the end of stream is reached during the initial skip. Once a significant character has been read the end of stream is treated as a terminator and the 'EndOfStream' exception is caught within the get routine. *) EXCEPTION Invalid(Text.T); CONST Decimal = 10; PROCEDURE GetCard( s: Stream; base: Fmt.Base := (*Fmt.*)Decimal; READONLY skip, terminate := CharType.WhiteSpace; unget := TRUE) : CARDINAL RAISES {Error, EndOfStream, Invalid}; (* Get a cardinal *) PROCEDURE GetBasedCard( s: Stream; READONLY skip, terminate := CharType.WhiteSpace; unget := TRUE) : CARDINAL RAISES {Error, EndOfStream, Invalid}; (* Like 'GetCard' but allows the number to contain a base e.g. 16_ff meaning ff with base 16. The base must be one of the bases specified by the 'Fmt.Base' type. *) PROCEDURE GetInt( s: Stream; base: Fmt.Base := (*Fmt.*)Decimal; READONLY skip, terminate := CharType.WhiteSpace; unget := TRUE) : INTEGER RAISES {Error, EndOfStream, Invalid}; (* Like 'GetCard' but allows the number to be preceded by '-' or '+'. If the number is preceded by '-' it is negated *) PROCEDURE GetBasedInt( s: Stream; READONLY skip, terminate := CharType.WhiteSpace; unget := TRUE) : INTEGER RAISES {Error, EndOfStream, Invalid}; (* Like 'GetBasedCard' but allows the number to be preceded by '-' or '+'. If the number is preceded by '-' it is negated *) PROCEDURE GetReal( s: Stream; READONLY skip, terminate := CharType.WhiteSpace; unget := TRUE) : REAL RAISES {Error, EndOfStream, Invalid}; (* Get a real number. The format of a real number is [+|-]d{d}[.{d}][(e|E)[+|-]d{d}] where 'd' is any decimal digit i.e. an optional sign, then a string of digits optionally containing a decimal point, then an optional e or E followed by an optional sign, followed by an integer. *) PROCEDURE GetLongReal( s: Stream; READONLY skip, terminate := CharType.WhiteSpace; unget := TRUE) : LONGREAL RAISES {Error, EndOfStream, Invalid}; (* Get a long real. The format of long reals is the same as that of reals *) PROCEDURE GetBool( s: Stream; READONLY skip, terminate := CharType.WhiteSpace; unget := TRUE) : BOOLEAN RAISES {Error, EndOfStream, Invalid}; (* Get a boolean. The significant characters read must be either "TRUE" or "FALSE" (case is not important) *) END IO.