io

tape
import io from "io";

io.println("Hello, {name}!");
io.eprintln("warning: no config file");

let line: ?string = io.read_line() or { return 1; };   // null at the end of input

io is the console, the streams under files, pipes and sockets, and IoError, the error that io, fs, os, process and net all report. It is a capability module: it touches the outside world.

Printing

FunctionWrites to
print(s: []const u8)stdout
println(s: []const u8)stdout, then a newline
eprint(s: []const u8)stderr
eprintln(s: []const u8)stderr, then a newline

Any string, literal or []const u8 works, and interpolation builds the text: io.println("{count} files, {size} bytes"). A C string from a C library goes through cstr.as_view(p) first.

Printing is not buffered: every call goes straight to the console, so nothing is lost when main returns or the program traps. For many small writes, use a BufWriter (below).

Reading lines

tape
while (true) {
    let line: ?string = io.read_line() or { return 1; };
    if (line == null) { break; }        // end of input
    let text: string = line!;
    io.println("got: {text}");
}

read_line() -> ?string or IoError returns the next line of stdin without its line ending ("\n" or "\r\n"), and null at the end of the input. The last line doesn’t need a newline. It reads one byte at a time, so it never takes more of stdin than the line it returns.

Streams

A Reader reads from, and a Writer writes to, a file descriptor: the standard streams or an open file (fs.File.reader(), fs.File.writer()); or a socket, a TCP connection’s (net.TcpStream.reader(), writer()); or a hook, a function with a context, which is how a TLS connection (net.TlsStream.reader(), writer()) is a stream.

tape
pub enum StreamKind : u8 { Descriptor = 0; Socket = 1; Hook = 2; }
pub struct Reader { fd: i64; kind: StreamKind = StreamKind.Descriptor;
                    ctx: *u8 = null; read_fn: fn(*u8, *u8, i64) -> i64 = null; }
pub struct Writer { fd: i64; kind: StreamKind = StreamKind.Descriptor;
                    ctx: *u8 = null; write_fn: fn(*u8, *const u8, i64) -> i64 = null; }
pub fn hook_reader(ctx: *u8, read_fn: fn(*u8, *u8, i64) -> i64) -> Reader;
pub fn hook_writer(ctx: *u8, write_fn: fn(*u8, *const u8, i64) -> i64) -> Writer;

kind says which: Reader { fd: f.fd; } is a descriptor, a socket’s views carry StreamKind.Socket so their reads and writes go to the socket (Windows sockets are not file descriptors), and a hook’s call its function with its context: read_fn(ctx, buf, len) returns the bytes read (0 at the end), write_fn(ctx, buf, len) the bytes taken, either a negative IoError value (-(e as i64)) on failure. Everything below, the buffered types included, works on all three. A Reader or Writer doesn’t own what it reads or writes: whoever opened it closes it.

tape
// A Writer that counts what goes through it
struct Counter { n: i64; }
fn count(ctx: *u8, buf: *const u8, len: i64) -> i64 {
    var c: *Counter = ctx as *Counter;
    c.n = c.n + len;
    return len;
}

var c: Counter = Counter { n: 0; };
let w: io.Writer = io.hook_writer(&c as *u8, count);
w.write_all("hello") or {};          // c.n is 5
FunctionDoes
stdin() -> Reader, stdout() -> Writer, stderr() -> WriterThe standard streams
r.read(buf: []u8) -> i64 or IoErrorRead up to buf.len bytes; 0 at the end
r.read_all() -> string or IoErrorEverything up to the end
r.read_line() -> ?string or IoErrorThe next line, null at the end
w.write(data) -> i64 or IoErrorWrite some of data; returns how many bytes
w.write_all(data) -> void or IoErrorWrite all of data
w.flush() -> void or IoErrorNothing to do for a Writer; there for symmetry

write may take fewer bytes than you give it (a pipe, a signal); use write_all unless you handle that yourself.

Buffering

A BufWriter collects small writes and passes them on a buffer at a time. A BufReader reads a buffer at a time and hands out bytes and lines from it.

tape
var out: io.BufWriter = io.buf_writer(io.stdout());
for (i in 0..1000) {
    out.write_all("line {i}\n") or { return 1; };
}
out.close() or { return 1; };          // flushes, then frees the buffer
FunctionDoes
buf_writer(w: Writer) -> BufWriterAn 8 KiB (BUF_SIZE) buffer over w
buf_writer_sized(w, cap) -> BufWriterA buffer of cap bytes
b.write_all(data), b.write(data)Buffer data, flushing when it doesn’t fit
b.buffered() -> i64Bytes waiting in the buffer
b.flush() -> void or IoErrorWrite what is buffered
b.close() -> void or IoErrorFlush and free the buffer
buf_reader(r: Reader) -> BufReader, buf_reader_sized(r, cap)A buffered reader
b.read(buf), b.read_line(), b.read_all()As the Reader’s, a buffer at a time
b.close()Free the buffer

Buffered bytes are only written on flush or close: if the program ends first, they are lost. Closing a BufWriter or BufReader doesn’t close the file underneath. A capacity below 1, and any use after close, stop the program with a trap.

IoError

tape
let text: string = fs.read_file("config.ini") or {
    io.eprintln("cannot read config.ini: {err}");     // "cannot read config.ini: not found"
    return 1;
};
VariantDescription
NotFoundnot found
PermissionDeniedpermission denied
AlreadyExistsalready exists
NotADirectorynot a directory
IsADirectoryis a directory
DirectoryNotEmptydirectory not empty
InvalidInputinvalid input
UnexpectedEofunexpected end of file
BrokenPipebroken pipe
Interruptedinterrupted
NoSpaceno space left on device
Unsupportednot supported on this platform
ConnectionRefusedconnection refused
ConnectionResetconnection reset by peer
ConnectionAbortedconnection aborted
NotConnectednot connected
AddrInUseaddress in use
AddrNotAvailableaddress not available
NetworkUnreachablenetwork unreachable
HostUnreachablehost unreachable
TimedOuttimed out
WouldBlockoperation would block
HostNotFoundhost not found
OtherI/O error

The same failure is the same variant on every platform: opening a missing file is NotFound on Windows, Linux and macOS alike. The variants from ConnectionRefused to HostNotFound come from sockets (net).

Deprecated

print_i64, print_bool and parse_int still work and will be removed in a later release. Use interpolation, io.println("{n}"), and conv.parse_i64, which reports bad input instead of stopping at it.

Platforms

Everything works on Windows, Linux and macOS. On Uusi the console and files work (see fs); errors of the kernel’s own files are all Other for now.

Last modified: