Io v0.1.0

Source
Streams, console I/O, readers and writers

Installation

Add Io to the [Dependencies] table of your package's Rux.toml:

[Dependencies]
Io = { Namespace = "Rux", Version = "0.1.0" }

Standard packages are declared by hand: the registry does not serve them, so rux add Rux/Io cannot add them.

Dependencies

Guide

Streams, console I/O, readers and writers.

Console access is written directly against each platform's own primitives, selected at compile time: the write/read syscalls on Linux, macOS, and the BSDs, and WriteFile/ReadFile on Windows. There is no C runtime in the path.

What it provides

FunctionPurpose
PrintWrite a value to standard output
PrintLineWrite a value followed by a newline
ReadLineAppend one line from standard input
ReadLineFromAppend one line from any reader, byte by byte
ReadExactFill a byte slice from any reader
WriteAllSend a byte slice to any writer
ReadTextLineAppend one validated line from a buffered reader

Print and PrintLine accept values implementing Display from Rux/Text, along with their primitive and text overloads. The primitive overloads cover every boolean width from bool8 to bool512, the floats float8, float16, float32 and float64, the integers up to 64 bits with int and uint, and every character width from char8 to char512. float80 and wider have no overload, because their text allocates: render one through Rux/Format's Float80 to Float512 values with FromValue and FormatWith, passing the allocator. They return the failure of the write as an IoError? — none when everything was written — rather than a fallible, so a bare PrintLine("ready"); statement needs no handling while a caller who cares writes if PrintLine(report) is IoError { ... }.

Given arguments after it, the first text is a pattern: PrintLine("{} of {}", done, total) fills each {} or {:spec} placeholder from the next argument, and {{ and }} write a literal brace. The pattern parameter is marked #Format(), so when it is a string literal the compiler counts its placeholders against the arguments, and PrintLine("{} {}", 1) is a compile error rather than a silent failure at run time. A pattern known only at run time that does not match reports InvalidInput through the returned IoError? and prints nothing. With no arguments the text is not a pattern at all: PrintLine("{{}}") writes {{}} verbatim, braces and all.

Both line readers hold the same contract: a line feed ends a line, a carriage return immediately before it belongs to the ending, the last line needs no ending, and the line must be UTF-8. A line that is not is InvalidText, reported once the line has been consumed through its ending, so the next call starts on the next line. ReadLine and ReadLineFrom read one byte at a time and never take a byte past the line, which is right for standard input or any stream shared with whatever reads next; ReadTextLine reads through a BufferedReader and also bounds the line's length.

Every stream operation that can fail returns a native fallible with an IoError failure: Read succeeds with the number of bytes read, Write with the number taken, Seek with the new position, and Flush, ReadExact, WriteAll, and the line readers with (). The end of a stream is the EndOfStream failure, which IsEnd recognizes, and IsTransient names the interruptions worth retrying. An IoError renders through {} as its kind's words and the platform's code, as not found (os error 2).

Streams in memory

ByteReader reads a slice of bytes the caller holds and ByteWriter gathers what is written into a block that grows as needed, so code written against Reader and Writer can run without a file or a socket. Both seek within what they hold: a seek back in a ByteWriter overwrites in place, and a seek past the end is refused with InvalidInput. A ByteReader ends with EndOfStream like any other stream.

Text and streams

Display writes into a TextWriter and a stream takes bytes, so something has to stand between them. ConsoleWriter is that adapter over standard output and StreamWriter is it over any Writer — a file, a buffered writer, a pipe — which is what lets a value describe itself anywhere other than standard output or memory:

import Core::Failed;
import Io::{ IoError, StandardOut, StreamWriter, WriteValueLine };
import Text::TextWriter;

func Main() -> int {
    var out = StandardOut::Acquire();
    let measurement: int32 = 42;

    // The adapter turns a stream into a text writer. It cannot return an I/O error through the text protocol, so
    // it keeps the first one it saw in `failure` — which is why that is checked afterwards rather than at the call.
    var failure: IoError? = none;
    var adapter = StreamWriter(out, @failure);
    let text: &var TextWriter = adapter;

    // The type argument is required: the function is generic over the value so that it can borrow it rather than
    // take ownership, and a generic bound is not inferred from an interface value.
    let written = WriteValueLine<int32>(text, measurement);
    if failure is IoError || Failed(written) {
        return 1;
    }
    return 0;
}

Neither error type can hold the other's failures, so the crossing is made explicitly in both directions. FromIoError turns what a stream reported into what a formatter understands: success stays success and every failure becomes WriterFailure, which is all FormatError can say about a destination. That is lossy on purpose, and the adapter makes the loss good by recording the original IoError in an optional slot the caller holds, none until something fails — the formatter learns the one thing it can act on, and the caller learns exactly what happened. Once a failure is recorded the stream is not touched again, so a value part-way through rendering stops rather than writing into a destination that has already refused.

ToIoError comes back the other way, and the distinction it preserves is the one that decides what to do next: a request that was wrong is InvalidInput and will be wrong again, while a destination that failed came back as an IoError in the first place. Bytes that were not text are InvalidText; a bounded destination that filled is LimitExceeded. Nothing is stuffed into IoError's raw, which is errno or the Win32 code and is meant to be looked up; a caller who needs to know which formatting failure it was builds a writer and calls Text::WriteValue themselves.

WriteValueLine and WriteFormatLine write a value or a pattern followed by a newline, and write the newline only if what came before it went out whole. A line is a value and its terminator together, so a value that could not be written must not be followed by one. Every PrintLine overload follows the same rule. WriteValueLine takes its value by bound rather than as an interface value, so the value is borrowed rather than given up: binding a move-only value to a Display transfers ownership of it, which would let such a value be written exactly once.

Stream helpers borrow concrete implementations directly as &var Reader or &var Writer interface views, and a named stream passed straight into one of them borrows without any binding: WriteAll(file, contents)?. The borrow neither copies nor consumes the stream, and it ends at the call. BufferedReader and BufferedWriter must keep their streams after construction, so they store ordinary interface handles; a stream that cannot be stored that way — because binding it would move it — supplies a stored adapter of its own, such as FileSystem::FileStream, whose representation is private and whose lifetime contract is documented where it lives. BufferedReader::New and BufferedWriter::New fail with Other when their buffer cannot be allocated. Both buffered types prohibit copying, move with <-, and release their owned buffers through ~BufferedReader and ~BufferedWriter.

Example

import Allocator::{ Allocator, SystemAllocator };
import Io::{ PrintLine, ReadLine };
import Text::StringBuilder;

func Main() -> int {
    var system = SystemAllocator();
    let allocator: Allocator = system;
    var name = StringBuilder(allocator);
    PrintLine("What is your name?");
    ReadLine(name) catch { else => return 1 };
    let text = name.IntoString();
    PrintLine(text);
    return 0;
}

Index

Interfaces

NameSummary
ReaderSomewhere bytes come from.
WriterSomewhere bytes go.
SeekerA stream whose position can be moved.

Types

NameSummary
EndianA byte order: which end of a multi-byte value comes first.
BufferedReaderA reader that fills a buffer in large reads and serves small ones from it.
BufferedWriterA writer that gathers small writes into a buffer and sends it in one piece.
IoErrorKindWhat an I/O operation reported.
IoErrorA structured I/O failure: the shared meaning, and the platform's own code beside it.
ByteReaderA reader of a slice of bytes, from its start.
ByteWriterA writer that keeps what is written in a growing block of memory.
StandardInStandard input, with the console-versus-redirection question answered at acquisition.
StandardOutStandard output: a byte Writer whose encoding matches where the bytes land.
StandardErrorStandard error: the same contract as standard output, on the stream reports belong to.
SeekFromWhere a seek starts counting from.
ConsoleWriterA TextWriter over standard output that records the stream's failure where the caller can see it.
StreamWriterA TextWriter over any stream, recording the stream's failure where the caller can see it.

Functions

NameSummary
ReadWordReads a width-byte unsigned word in the given order, into the low bytes of the result.
WriteWordWrites the low width bytes of value in the given order.
ReadUint8Reads a uint8 in the given order.
WriteUint8Writes a uint8 in the given order.
ReadInt8Reads an int8: the unsigned bytes, reinterpreted in two's complement.
WriteInt8Writes an int8: its two's-complement bytes, unchanged.
ReadUint16Reads a uint16 in the given order.
WriteUint16Writes a uint16 in the given order.
ReadInt16Reads an int16: the unsigned bytes, reinterpreted in two's complement.
WriteInt16Writes an int16: its two's-complement bytes, unchanged.
ReadUint32Reads a uint32 in the given order.
WriteUint32Writes a uint32 in the given order.
ReadInt32Reads an int32: the unsigned bytes, reinterpreted in two's complement.
WriteInt32Writes an int32: its two's-complement bytes, unchanged.
ReadUint64Reads a uint64 in the given order.
WriteUint64Writes a uint64 in the given order.
ReadInt64Reads an int64: the unsigned bytes, reinterpreted in two's complement.
WriteInt64Writes an int64: its two's-complement bytes, unchanged.
ReadUint128Reads a uint128 in the given order.
WriteUint128Writes a uint128 in the given order.
ReadInt128Reads an int128: the unsigned bytes, reinterpreted in two's complement.
WriteInt128Writes an int128: its two's-complement bytes, unchanged.
ReadUint256Reads a uint256 in the given order.
WriteUint256Writes a uint256 in the given order.
ReadInt256Reads an int256: the unsigned bytes, reinterpreted in two's complement.
WriteInt256Writes an int256: its two's-complement bytes, unchanged.
ReadUint512Reads a uint512 in the given order.
WriteUint512Writes a uint512 in the given order.
ReadInt512Reads an int512: the unsigned bytes, reinterpreted in two's complement.
WriteInt512Writes an int512: its two's-complement bytes, unchanged.
ReadFloat32Reads a float32 as the IEEE 754 bit pattern of its width.
WriteFloat32Writes a float32 as the IEEE 754 bit pattern of its width.
ReadFloat64Reads a float64 as the IEEE 754 bit pattern of its width.
WriteFloat64Writes a float64 as the IEEE 754 bit pattern of its width.
PrintValueWrites a value's text to standard output.
PrintValueLineWrites a value's text to standard output, then a newline.
PrintWrites a value's text to standard output.
PrintLineWrites a value's text to standard output, then a newline.
ReadLineAppends the next line of standard input to builder, without its ending.
ReadLineFromAppends the next line of reader to builder, without its ending, reading one byte at a time.
ReadExactFills all of into, looping over partial reads, or says why it could not.
WriteAllWrites all of bytes, looping over partial writes, or says why it could not.
FromIoErrorTurns what a stream reported into what a formatter understands.
ToIoErrorTurns a formatting failure into something a stream's caller can act on.
WriteValueLine<T>Writes a value's text, then a newline, and writes the newline only if the value went out whole.
WriteFormatLineWrites format with each {} replaced by the next argument, then a newline, on the same rule.
ReadTextLineAppends the next line of reader to builder, without its ending.

Constants

NameSummary
DefaultBufferCapacityThe room a buffered stream takes when the caller expresses no preference.
ZeroProgressLimitHow many consecutive zero-progress rounds the looping helpers tolerate before refusing.