IO

Sync byte interfaces, descriptor-backed standard handles, and MemoryStream.

std.io defines sync byte-oriented interfaces, descriptor-backed standard handles, descriptor wrappers, and in-memory streams. Async byte streams live in std.aio.

import std.io;

fn main() {
    io.println("hello");
}

io.stdin, io.stdout, and io.stderr borrow the process descriptors. They do not close those descriptors when dropped.

Standard Handles

The standard handles expose fallible read and write helpers:

try io.stdout.write("hello\n");
try io.stderr.write("error\n");

let line = try io.stdin.read_line();
let text = try io.stdin.read_text(32);
let n = try io.stdin.read(buffer);

write(...) must be handled with try or catch. read(buffer) returns a byte count; 0 means EOF. read_text(byte_count) reads up to the requested byte count and returns owned String. read_text and read_line validate bytes as Zynx UTF-8 text without embedded NUL bytes, returning utf8.UnicodeError.Invalid for malformed text. read_line() reads until newline, excludes \n, strips a preceding \r, and returns IOError.UnexpectedEofError only when EOF happens before any byte is read.

For simple human-facing CLI output, use the text helpers:

import std.io;

fn main() {
    let package = "demo";
    let missing = "zynx.lock";

    io.println("package: {package}");
    io.eprintln("missing file: {missing}");
}

io.print, io.println, io.eprint, and io.eprintln write borrowed text to stdout or stderr and intentionally ignore low-level write status. Use Writer.write or io.write when a program must handle I/O errors. Use string interpolation to prepare formatted text before passing it to these helpers.

Sync Interfaces

interface Reader {
    fn read(self, buffer: [u8]) throws(IOError | AllocError) -> usize;
}

interface Writer {
    fn write(self, data: bytes.Bytes) throws(IOError | AllocError) -> void;
}

interface Stream: Reader, Writer;

Reader.read(buffer) returns the number of bytes read. Writer.write(data) writes all supplied bytes or returns an error. Writer APIs use canonical bytes.Bytes inputs; callers can pass string literals, String, existing byte views, or [u8] slices directly.

The shared helpers operate on sync interfaces:

ItemPurpose
io.print(text), io.println(text)Write text to stdout, with println appending \n; low-level write status is ignored.
io.eprint(text), io.eprintln(text)Write text to stderr, with eprintln appending \n; low-level write status is ignored.
io.write(writer, data)Write byte-view data to any Writer.
io.read(reader, buffer)Fill the requested buffer exactly or return IOError.UnexpectedEofError.
import std.io;
import std.mem;

fn main() {
    let stream = io.MemoryStream();
    try io.write(&stream, "abc");
    stream.reset();

    let exact = [0 as u8, 0 as u8, 0 as u8];
    try io.read(&stream, mem.slice_of<u8>(exact.ptr, exact.length));
    assert exact[2] == 99 as u8;
}

Descriptor Wrappers

DescriptorReader, DescriptorWriter, and DescriptorStream wrap a numeric descriptor without taking ownership of it.

import std.io;
import std.os;

fn main() {
    let fds = [0, 0];
    unsafe {
        assert os.pipe(fds.ptr) == 0;
    }

    let reader = io.DescriptorReader(fds[0] as usize);
    let writer = io.DescriptorWriter(fds[1] as usize);
    try writer.write("ok\n");

    let line = try reader.read_line();
    assert line.str() == "ok";

    unsafe {
        _ = os.close(fds[0] as usize);
        _ = os.close(fds[1] as usize);
    }
}
MethodPurpose
DescriptorReader(descriptor), DescriptorWriter(descriptor), DescriptorStream(descriptor)Create borrowed sync wrappers.
descriptor()Return the borrowed descriptor.
read(buffer)Read up to buffer.length bytes and return the byte count.
read_text(byte_count)Read up to a byte count and return owned String.
read_line()Read until newline, excluding \n and stripping a preceding \r.
write(data)Write all bytes from a canonical bytes.Bytes input.

MemoryStream

MemoryStream is an owned in-memory sync Stream. Writes append to a growable buffer, reads consume from a cursor, and reset() moves the cursor back to the beginning.

import std.io;
import std.mem;
import std.bytes;

fn main() {
    let stream = try io.MemoryStream(bytes.Bytes("hi"));
    let buffer = [0 as u8, 0 as u8];

    assert (try stream.read(mem.slice_of<u8>(buffer.ptr, buffer.length))) == 2;
    assert buffer[0] == 104 as u8;
    assert buffer[1] == 105 as u8;
}
MethodPurpose
MemoryStream()Create an empty stream with the default allocator.
MemoryStream(allocator)Create an empty stream with custom storage.
MemoryStream(data)Create a stream containing a copy of a byte view.
read(buffer)Copy bytes from the current cursor and return the byte count.
write(data)Append bytes to the stream.
len(), capacity()Inspect buffered size and allocated capacity.
position(), remaining()Inspect the read cursor.
is_empty()Return whether the stream has no buffered bytes.
stream.bytes()Borrow all buffered bytes as a read-only byte view.
reset(), clear()Move the cursor or remove buffered bytes.
reserve(capacity), shrink()Adjust backing storage.

Errors

IOError includes common file and stream failure cases such as FileNotFoundError, PermissionError, InterruptedError, TimeoutError, and UnexpectedEofError. std.aio re-exports the same IOError type for async byte streams.