I/O Operations

The underlying I/O routines of the operating system act on collections of bytes, which are 8-bit quantities that can be thought of as integers in the range 0..255. The Magma I/O routines provide three different interpretations of these collections of bytes: as strings (textual data), as byte sequences, and as representations of Magma objects. The first two of these are unstructured --- any amount of data is a valid instance --- while the third viewpoint requires that the byte values conform to a certain protocol. This protocol is described in a separate document (Magma Object Transfer Protocol), available from the Magma website.

To make error handling easier, most of the I/O operations that read or write data also have checked variants that will not raise an error due to I/O failure but instead return whether the operation was successful.

Contents

End of File Marker

The I/O operations described below that return strings need some way of indicating when a read request fails due to no more data being available. This is achieved by returning a special "end of file" (shortened to "EOF") string that is not equal to any normal string.

Eof() : -> MonStgElt
Creates the special EOF string.
IsEof(S) : MonStgElt -> BoolElt
Given a string S, return whether S is the special EOF string.
AtEof(I) : IO -> BoolElt
Returns whether all data is known to have been read from I (and thus that further reads will return the special EOF string). Note that if this function returns false then it may still be the case that the next read returns EOF; typically AtEof only returns true when a previous read has already returned EOF.

Example IO_EOF (H3E14)

We create the special EOF string and test it.
> eof := Eof();
> eof;
EOF
> IsEof(eof);
true
Note that the special EOF string is different from the string "EOF", which is a normal string.
> E := "EOF";
> E;
EOF
> IsEof(E);
false

Strings

Read(I : parameters) : IO -> MonStgElt
    Max: RngIntElt                      Default: 0
Waits for data to become available for reading from I and then returns it as a string. If the parameter Max is set to a positive value then at most that many bytes will be read. Note that fewer than Max bytes may be returned, depending on the amount of currently available data.

If no data can be read from I then the special EOF string is returned.

ReadCheck(I : parameters) : IO -> BoolElt, MonStgElt
    Max: RngIntElt                      Default: 0
Performs the same Read action as above. If an error would arise then instead false is returned; otherwise true is returned along with the result of the read.
Read(I, n) : IO, RngIntElt -> MonStgElt
Attempts to read exactly n bytes of data from I and then returns it as a string; will wait if necessary until either n bytes have become available or the input has terminated. Note that the returned string may contain fewer than n bytes if the input has terminated.

If the input terminates and no bytes have been read then the special EOF string is returned.

ReadCheck(I, n) : IO, RngIntElt -> BoolElt, MonStgElt
Performs the same Read action as above. If an error would arise then instead false is returned; otherwise true is returned along with the result of the read.
Write(I, s) : IO, MonStgElt ->
Writes the characters of the string s to I.
WriteCheck(I, s) : IO, MonStgElt -> BoolElt
Performs the same Write action as above. Returns whether the write completed without error.

Legacy String Operations

The I/O operations in this section are retained for compatibility with older versions of Magma. They reflect the semantics of the C standard I/O library at the time. Their use is somewhat discouraged, although there is currently no good alternative for getting a line of input at a time (as provided by Gets).

Put(I, s) : IO, MonStgElt ->
Writes the string s to the channel I. This is equivalent to Write(I, s).
Puts(I, s) : IO, MonStgElt ->
Writes the string s to the channel I, followed by a newline character. This is equivalent to Write(I, s cat "\n").
Getc(I) : IO -> MonStgElt
Reads a single character from I and returns it as a string. This is equivalent to Read(I, 1). If no data is available from I then the special EOF string is returned instead.
Gets(I) : IO -> MonStgElt
Reads a line of input from I and returns it as a string. The line is considered to end when either a newline character is received or all data in I has been received. If a newline character is received then it is omitted from the returned string. If no data at all is available from I then the special EOF string is returned instead.

This routine is only currently available for files and pipes.

Ungetc(I, c) : IO, MonStgElt ->
Pushes the given character (a string of length one) c into the start of the input buffer of I. The next read operation on I should return this character as the first byte.

This routine is only currently available for files and pipes.

Use of this intrinsic may interact unpredictably and poorly with asynchronous I/O, and especially so when reading objects. Its use is strongly discouraged and it will probably be removed in a future release.

Example IO_LineCount (H3E15)

We write a function to count the number of lines in a file. Note the method of looping over the lines of the file: we must get the line and then test whether it is the special EOF marker.
> function LineCount(F)
>     FP := Open(F, "r");
>     count := 0;
>     while true do
>         line := Gets(FP);
>         if IsEof(line) then
>             break;
>         end if;
>         count +:= 1;
>     end while;
>     return count;
> end function;
> LineCount("/etc/passwd");
59

Byte Sequences

A byte sequence is a sequence of integers in the range 0..255. Note that byte sequences have the ring of integers as their universe; they will never be null.

ReadBytes(I : parameters) : IO -> SeqEnum
    Max: RngIntElt                      Default: 0
Waits for data to become available for reading from I and then returns it as a byte sequence. If the parameter Max is set to a positive value then at most that many bytes will be read. Note that fewer than Max bytes may be returned, depending on the amount of currently available data.

If no data can be read from I then an empty byte sequence is returned.

ReadBytesCheck(I : parameters) : IO -> BoolElt, SeqEnum
    Max: RngIntElt                      Default: 0
Performs the same ReadBytes action as above. If an error would arise then instead false is returned; otherwise true is returned along with the result of the read.
ReadBytes(I, n) : IO, RngIntElt -> SeqEnum
Attempts to read exactly n bytes of data from I and then returns it as a byte sequence; will wait if necessary until either n bytes have become available or the input has terminated. Note that the returned sequence may contain fewer than n bytes if the input has terminated.

If the input terminates and no bytes have been read then an empty byte sequence is returned.

ReadBytesCheck(I, n) : IO, RngIntElt -> BoolElt, SeqEnum
Performs the same ReadBytes action as above. If an error would arise then instead false is returned; otherwise true is returned along with the result of the read.
WriteBytes(I, S) : IO, SeqEnum ->
Writes the bytes of the byte sequence S to I.
WriteBytesCheck(I, S) : IO, SeqEnum -> BoolElt
Performs the same WriteBytes action as above. Returns whether the write completed without error.

Objects

Some Magma objects can be used in I/O calls. The details of which objects can be handled, as well as specifications of the format, are provided in a separate document (Magma Object Transfer Protocol), available from the Magma website.

As of V25.1 the allowable object types include integers, rationals, reals, complexes, booleans, strings, finite field elements, polynomials (univariate and multivariate), sequences, sets, indexed sets, multisets, tuples, lists, records, matrices, vectors, lattices, permutations, number field elements, and all the associated parent structures. Support for more types of objects will continue to be added in patch releases.

ReadObject(I) : IO -> Any
Reads a Magma object from I in Magma Object Format and returns it. An error will be raised if input terminates before a complete object can be read.
ReadObjectCheck(I) : IO -> BoolElt, Any
Performs the same ReadObject action as above. If an error would arise then instead false is returned; otherwise true is returned along with the result of the read.
WriteObject(I, x) : IO, Any ->
Writes the Magma object x to I, using the Magma Object Format.
WriteObjectCheck(I, x) : IO, Any -> BoolElt
Performs the same WriteObject action as above. Returns whether the write completed without error.

Versions

As described in the Magma Object Transfer Protocol document (available from the Magma website), each program that can read or write Magma objects does so according to the Protocol. This Protocol will continue to evolve as more objects are implemented and as more features are added.

In order to manage the different versions of the Protocol, each is associated with a unique version number (or simply version), and this version forms part of the data for each object. Programs that understand one version of the Protocol are also expected to understand all previous versions. Thus programs can detect if a received object uses a later version of the Protocol and raise an error, or cooperate to ensure that communication takes place using only a mutually-understood version of the Protocol.

In the following, a version number is represented as a string of the form "M.m", where M and m are integers between 0 and 65 535 (inclusive). When two programs wish to exchange Magma objects, the first step should be for each to notify the other of the version of the Protocol that it understands. Each program can then send objects using the lower of the two versions, ensuring that the object data is correctly interpreted by the other program.

GetMOTPMaxVersion() : -> MonStgElt
Returns the maximum (latest) version number of the Protocol that is understood by this copy of Magma.
GetMOTPVersion() : -> MonStgElt
Returns the version number of the Protocol that will be used by Magma for newly-created I/O objects. This will usually be the same as the maximum version, but the user can explicitly lower it by using SetMOTPVersion.
SetMOTPVersion(v) : MonStgElt ->
Sets the version of the Protocol to use with newly-created channels. This may not be set to a value higher than the maximum version (as returned by GetMOTPMaxVersion). Usually it is not desirable to change this value as version negotiation takes care of communication issues, but it can be useful when testing or when saving objects that are to be read later by programs that only understand an earlier version of the Protocol.
SetMOTPVersion(I, v) : IO, MonStgElt ->
Sets the version of the Protocol to use from now on with the given channel. Usually it is not desirable to change this value as version negotiation takes care of communication issues, but it can be useful when testing or when saving objects that are to be read later by programs that only understand an earlier version of the Protocol.
NotifyVersion(I) : IO ->
Sends a version Protocol message to the other end of the channel that contains the desired version number to use for communication. i.e., a version number that this program will be able to understand. This will be either the value returned by GetMOTPVersion when the channel was created, or the value used in a call to SetMOTPVersion on the channel.

It is allowed, but unusual, to send versions in this way after the initial exchange of versions occurs. The other end of the channel is expected to update its internal value to use for communication when it receives such a message. For more details see the Magma Object Transfer Protocol document.

ExpectVersion(I) : IO ->
Expects a version Protocol message containing a version number understood by the other end of the communication channel. This version will be stored internally and Protocol messages sent to the other end of the channel will use a version no higher than this value. An error will arise if a different Protocol message was received when a version was expected.
ExchangeVersions(I) : IO ->
Performs both a NotifyVersion and ExpectVersion on the channel. This is the preferred method to use; after a communication channel has been set up then the first thing both sides should do is to call ExchangeVersions (or its asynchronous version, see later) on the channel. Thereafter objects can be safely sent and received.

Note: It is often the case that one is using the same version of Magma on each machine, and thus that each process understands the same version of the Protocol. It may thus be tempting to omit the use of ExchangeVersions under such circumstances, and this is reasonable for interactive sessions. However, it is strongly encouraged to use ExchangeVersions in longer-lived code; doing so will save many headaches down the line when one finds oneself trying to get different versions of Magma to interact.

Example IO_ObjectExchange (H3E16)

We demonstrate the appropriate process to use for the simple exchange of objects between two Magma processes. First we create the server socket and wait for the connection. In this case we explicitly give the local host and port to use in order to slightly simplify the example.
> // First Magma process
> server := Socket(: LocalHost := "localhost", LocalPort := 4000);
> S1 := WaitForConnection(server);
The first Magma process is now waiting for a connection to be made. We create a client socket in another Magma process on the same machine and establish the connection with the first process. Since we intend to exchange objects over this connection, we also do the version exchange immediately.
> // Second Magma process
> S2 := Socket("localhost", 4000);
> ExchangeVersions(S2);
Now the second Magma process is waiting for the version exchange to be complete. The first process has received the connection and completes the version exchange.
> // First Magma process
> ExchangeVersions(S1);
Both processes are now ready to communicate using ReadObject and WriteObject. We demonstrate some simple exchanges.
> // First Magma process
> WriteObject(S1, [ IsPrime(x) : x in [1..10] ]);
> // Second Magma process
> ReadObject(S2);
[ false, true, true, false, true, false, true, false, false, false ]
> WriteObject(S2, IdentityMatrix(Rationals(), 2));
> // First Magma process
> ReadObject(S1);
[1 0]
[0 1]
> Parent($1);
Full Matrix Algebra of degree 2 over Rational Field
It is possible to use eval and Magma-level printing in order to send objects of types that not yet handled by the Protocol.
> // First Magma process
> E := EllipticCurve("37a1");
> E;
Elliptic Curve defined by y^2 + y = x^3 - x over Rational Field
> WriteObject(S1, E);
>> WriteObject(S1, E);
              ^
Runtime error in 'WriteObject': Unable to serialize specified object
> WriteObject(S1, Sprint(E, "Magma"));
> // Second Magma process
> E := eval ReadObject(S2);
> E;
Elliptic Curve defined by y^2 + y = x^3 - x over Rational Field

Positioning

Some I/O types have positioning information associated with them, and this information can be retrieved and modified to allow access to specific parts of the data. Essentially, we regard the object as equivalent to a large array of bytes and the position as being the index within that array that the next I/O call will start from.

I/O types that support this view are called seekable, after the C library function for manipulating the position. Currently only file objects are seekable. Note: The position starts at 0, not 1, reflecting the C conventions of the underlying operating system calls.

Tell(I) : IO -> RngIntElt
Given a seekable channel I, returns the current position as an integer.
Seek(I, offset) : IO, RngIntElt ->
Seek(I, offset, origin) : IO, RngIntElt, RngIntElt ->
Adjusts the position of the seekable channel I to be at the given offset relative to the origin. The origin must be one of: 0 to mean relative to the beginning; 1 to mean relative to the current position; or 2 to indicate relative to the end of the channel. If the origin is not specified then it is taken to be 0.
Rewind(I) : IO ->
Moves the position of the seekable channel I to the beginning. This is equivalent to Seek(I, 0).

Other I/O Operations

Flush(I) : IO ->
This routine ensures that all pending output on the given channel has been committed. It is useful to ensure that previously-committed asynchronous writes have completed, and also to ensure that any buffered data has actually been written.
V2.28, 13 July 2023