Skip to content

std.io

std.io defines byte-oriented interfaces, nominal process standard handles, and the smallest shared helpers for concrete stream types. Interfaces describe read and write capabilities without exposing the host identity of a resource.

Contract

  • std.io separates byte capabilities from concrete storage and resources.
  • io.read and io.write work with any value that implements the sync byte interfaces.
  • Concrete types such as MemoryStream, process standard handles, pipes, and TCP streams provide storage or nominal resources.
  • Async byte streams live in std.async.io.

Migration status: RFC 0054 removes ByteBuffer from the target. Owning ^[u8] plus its OutputSpan<u8> initialization session replace the duplicate growable-buffer abstraction and the unsound storage/set_len pair. The checked-in implementation remains transitional. RFC 0058 additionally removes every custom-allocator constructor and field. Remaining owning buffers use the compiler/runtime-private built-in allocation domain. RFC 0064 gives the historical ByteBuffer.append no compatibility alias. MemoryStream.write remains write: it implements the stream protocol even though this concrete writer places bytes at its end. RFC 0065 removes DescriptorReader, DescriptorWriter, and DescriptorStream. Safe portable std.io neither accepts nor returns an OS resource identity as an integer.

Standard Handles

  • stdin, stdout, and stderr are opaque process-lifetime values of nominal Stdin, Stdout, and Stderr types. They cannot be constructed, closed, or converted to a raw host identity.
  • read_text and read_line validate bytes as Zynx text and throw std.unicode.utf8.UnicodeError.Invalid only for malformed UTF-8; U+0000 is preserved as ordinary counted text.
  • print, println, eprint, and eprintln write borrowed text to stdout or stderr and intentionally ignore low-level write status; they are convenience helpers for human-facing output, not a replacement for Writer.write when a program must handle I/O errors.
  • Text output is size-aware. These helpers use the view's pointer plus size; they neither scan for NUL nor assume text.ptr[text.size] == 0.
  • Use explicitly owning string interpolation to prepare formatted text before passing it to those helpers. It requires ^, allocates, and reports AllocError; see String Interpolation And Formatting for the syntax and format specifiers.
Zynx
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);
Zynx
import std.io;
import std.mem.{ AllocError };

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

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

Sync Interfaces

Reader reads bytes into caller-provided storage. A return of 0 means end-of-stream for stream-shaped handles. Writer writes all supplied bytes or returns an error. Stream combines both capabilities.

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

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

interface Stream: Reader, Writer;

Generic code can accept any sync byte reader through an interface bound:

Zynx
import std.io;
import std.bytes;
import std.mem.{ AllocError };

fn read_once<S: io.Reader>(stream: ^S, buffer: &[u8]) throws(io.IOError | AllocError) -> usize {
	return try stream.read(buffer);
}

fn main() {
	let stream = try io.MemoryStream(bytes.Bytes("z"));
	var buf = [0 as u8; 1];
	let buffer: &[u8] in _ = &buf[0..<buf.length];
	let n = try read_once(<-stream, buffer);

	assert n == 1;
	assert buf[0] == 122;
}

Read And Write Semantics

Reader.read(buffer) is a single overwrite operation on already initialized bytes. It may return fewer bytes than buffer.length, even when the stream has not reached EOF. Passing an empty buffer returns 0. For stream-shaped readers, a 0 result from a non-empty buffer means EOF. Callers that need an exact byte count must use io.read or write an explicit loop.

The Reader.read(output) overload initializes the unused tail of an OutputSpan<u8>. A successful call advances exactly the returned n-byte prefix. An error advances zero bytes; partial progress is returned as success and any later error is observed by a later call. A nonempty output session that returns 0 has the same stream EOF meaning. The reader neither commits nor cancels the caller's session.

Every concrete Reader implements both overloads. There is no default adapter: turning initialized overwrite into fresh output would require hidden scratch allocation or would pretend that spare capacity already contains live bytes.

io.read(reader, buffer) is the exact-fill helper. It repeatedly calls Reader.read until the whole buffer is filled. If EOF is observed first, it throws IOError.UnexpectedEof; bytes already copied into buffer remain written and are not rolled back.

Writer.write(data) is a complete-write operation. It either writes every byte in data or throws an error. Resource-backed implementations handle host-level partial progress internally without exposing an integer handle. MemoryStream.write appends all bytes or throws AllocError.

Byte Helpers

io.write writes a borrowed bytes.Bytes view, and io.read(reader, buffer) fills the requested buffer exactly or returns IOError.UnexpectedEof. Larger operations such as copying streams or reading until end-of-stream are explicit loops built from readers and writers.

Zynx
import std.bytes;
import std.io;
import std.mem.{ AllocError };

fn main() {
	var target = io.MemoryStream();
	try io.write(&target, bytes.Bytes("abc"));
	target.reset();

	var exact = [0 as u8; 3];
	let exact_view: &[u8] in _ = &exact[0..<exact.length];
	try io.read(&target, exact_view);
	assert exact[2] == 99 as u8;
}

Text Input Semantics

Standard-input text helpers sit at the explicit bytes-to-text boundary. They always validate bytes as counted UTF-8 before returning a ^str; U+0000 is preserved. Neither the returned owner nor a later borrowed view promises a terminator.

read_text(byte_count) performs one byte read of up to byte_count bytes. EOF before any byte is read returns an empty owning ^str. If the returned byte span is not complete valid UTF-8, including when byte_count cuts through a multibyte sequence, it throws utf8.UnicodeError.Invalid. It does not perform extra reads to complete a partial scalar value.

read_line() reads bytes until \n or EOF. The returned text excludes the trailing \n; a preceding \r is also stripped. EOF before any byte is IOError.UnexpectedEof, while EOF after at least one byte returns the final unterminated line. Because validation happens after the full line is collected, multibyte UTF-8 split across underlying byte reads is accepted when the final line is valid.

MemoryStream

MemoryStream is an owned in-memory Stream. It stores bytes in a growable buffer, writes append to the end, and reads consume from a read cursor. Use reset() to read buffered bytes again from the beginning.

Zynx
import std.bytes;
import std.io;
import std.mem.{ AllocError };

fn main() {
	var stream = io.MemoryStream();
	try stream.write(bytes.Bytes("abc"));

	var buf = [0 as u8; 2];
	let buffer: &[u8] in _ = &buf[0..<buf.length];
	let n = try stream.read(buffer);

	assert n == 2;
	assert buf[0] == 97 as u8;
	assert buf[1] == 98 as u8;
}

stream.bytes() returns a borrowed read-only view of all buffered bytes. The view is valid until the stream is mutated or dropped.

Zynx
import std.bytes;
import std.io;
import std.mem.{ AllocError };

fn main() {
	var stream = try io.MemoryStream(bytes.Bytes("hello"));
	let bytes = stream.bytes();

	assert stream.length == 5 as usize;
	assert bytes.at(0) == 104 as u8;

	stream.reset();
	stream.clear();
	assert stream.length == 0 as usize;
}

Buffered Streams

BufReader and BufWriter add owned buffering around any sync Reader or Writer. The default capacity is 8192 bytes; passing 0 selects a minimum one-byte buffer.

BufWriter.flush() writes buffered bytes to the wrapped writer and keeps the wrapper usable. BufWriter.finish() flushes buffered bytes and returns the wrapped writer, so it is the fallible completion operation for buffered output. BufWriter.into_inner() returns the wrapped writer without flushing. Any bytes left in the buffer are discarded, which is useful only when abandoning buffered output intentionally.

Ownership, Allocation, And Errors

stdin, stdout, and stderr are process-lifetime resources. User code cannot close them, move their host identity out, or construct another value from an integer. IOError is the portable error set used by sync byte streams. Host errors map into these cases and do not preserve a raw platform code or message. RFC 0070 adds the exact CrossDevice = 17 target case for a namespace rename which cannot cross its filesystem or volume; it is never hidden behind a copy-and-remove fallback. RFC 0072 adds Unsupported = 18 for an exact durability barrier whose inability can be discovered only from the concrete mounted filesystem or resource. RFC 0073 reuses that case when a concrete filesystem cannot uphold the certified atomic EOF placement promised by fs.AppendFile; a known target gap rejects its zynx-launch v4 grant before main instead. Neither operation silently falls back to weaker host behavior. Raw interop belongs to a future target-specific resource API, not portable safe std.io.

MemoryStream, BufReader, and BufWriter own their internal ^[u8] buffers and can throw AllocError when growing storage. Refill and append construction use OutputSpan<u8> instead of exposing spare capacity as initialized bytes. Partial read sizes, host readiness races, raw error values, buffering strategy, and capacity growth are not public API.

Use std.async.io for async byte streams, std.bytes for borrowed byte views, and std.fs and std.async.fs for capability-anchored blocking or cooperative-safe files.

API Reference

Errors

NameCasesDescription
IOErrorWouldBlock, BrokenPipe, ConnectionAborted, Connection, ConnectionRefused, ConnectionReset, CrossDevice, DirectoryNotEmpty, FileExists, NotFound, Interrupted, IsADirectory, NotADirectory, Permission, Timeout, UnexpectedEof, UnsupportedShared I/O error cases used by stream helpers and concrete handle types. CrossDevice = 17 and Unsupported = 18 are accepted target additions; implementation is pending. Unsupported reports a concrete resource that cannot provide a required exact durability barrier or certified atomic EOF append, never a silent weaker fallback.

Interfaces And Types

TypeKindPublic fieldsMethodsDescription
ReaderinterfaceN/AreadSync byte reader capability.
WriterinterfaceN/AwriteSync byte writer capability.
StreaminterfaceN/Aread, writeSync ordered byte stream capability.
BufReaderstruct: ReaderNonebuffered_length, fill, readBuffered reader over another sync reader.
BufWriterstruct: WriterNonebuffered_length, into_inner, flush, finish, writeBuffered writer over another sync writer.
Stdinopaque process-lifetime ReaderNoneread, read_text, read_lineStandard input with no constructor, raw identity, or close operation.
Stdoutopaque process-lifetime WriterNonewriteStandard output with no constructor, raw identity, or close operation.
Stderropaque process-lifetime WriterNonewriteStandard error with no constructor, raw identity, or close operation.
MemoryStreamstruct: Streamreadonly length, capacity, positionread, write, bytes, reset, clear, reserve, shrinkOwned in-memory stream backed by a growable byte buffer.

Functions

NameSignatureParametersReturnsDescription
printprint(text: str) -> voidtext: borrowed textvoidWrites text to stdout and ignores low-level write status.
printlnprintln(text: str) -> voidtext: borrowed textvoidWrites text and a trailing newline to stdout, ignoring low-level write status.
eprinteprint(text: str) -> voidtext: borrowed textvoidWrites text to stderr and ignores low-level write status.
eprintlneprintln(text: str) -> voidtext: borrowed textvoidWrites text and a trailing newline to stderr, ignoring low-level write status.
stdinstdin() -> StdinNoneStdinReturns a process-lifetime non-owning standard-input view.
stdoutstdout() -> StdoutNoneStdoutReturns a process-lifetime non-owning standard-output view.
stderrstderr() -> StderrNoneStderrReturns a process-lifetime non-owning standard-error view.
writewrite<W: Writer>(writer: &W, data: bytes.Bytes) throws(IOError | AllocError)writer: sync writer, data: bytes to writevoid throws(IOError | AllocError)Writes all bytes or returns an error.
readread<R: Reader>(reader: &R, buffer: &[u8]) throws(IOError | AllocError)reader: sync reader, buffer: mutable destination bytesvoid throws(IOError | AllocError)Fills the entire buffer or returns IOError.UnexpectedEof.

Interface Methods

MethodSignatureParametersReturnsDescription
Reader.readread(&self, buffer: &[u8]) throws(IOError | AllocError) -> usizebuffer: destination bytesusize throws(IOError | AllocError)Reads bytes synchronously and returns the number of bytes read.
Reader.readread(&self, output: &OutputSpan<u8>) throws(IOError | AllocError) -> usizeoutput: bounded uninitialized byte suffixusize throws(IOError | AllocError)Initializes and advances exactly the returned prefix; an error advances zero bytes.
Writer.writewrite(self, data: bytes.Bytes) throws(IOError | AllocError)data: bytes to writevoid throws(IOError | AllocError)Writes bytes synchronously or returns an error.

Buffered Constructors

NameSignatureParametersReturnsDescription
BufReaderBufReader<R: Reader>(source: R)source: wrapped readerBufReader<R>Creates a buffered reader with the default capacity.
BufReaderBufReader<R: Reader>(source: R, capacity: usize)source: wrapped reader, capacity: buffer capacityBufReader<R>Creates a buffered reader with an explicit capacity.
BufWriterBufWriter<W: Writer>(target: W)target: wrapped writerBufWriter<W>Creates a buffered writer with the default capacity.
BufWriterBufWriter<W: Writer>(target: W, capacity: usize)target: wrapped writer, capacity: buffer capacityBufWriter<W>Creates a buffered writer with an explicit capacity.

Buffered Methods

MethodSignatureParametersReturnsDescription
BufReader.buffered_lengthbuffered_length() -> usizeNoneusizeReturns unread bytes already held in the buffer.
BufReader.fillfill() throws(IOError | AllocError) -> usizeNoneusize throws(IOError | AllocError)Refills the buffer from the wrapped reader and returns bytes read.
BufReader.readread(buffer: &[u8]) throws(IOError | AllocError) -> usizebuffer: destination bytesusize throws(IOError | AllocError)Reads bytes from the buffer and wrapped reader.
BufWriter.buffered_lengthbuffered_length() -> usizeNoneusizeReturns bytes waiting in the buffer.
BufWriter.into_innerinto_inner() -> ^WNone^WReturns the wrapped writer without flushing; buffered bytes are discarded.
BufWriter.flushflush() throws(IOError | AllocError)Nonevoid throws(IOError | AllocError)Writes buffered bytes to the wrapped writer and clears the buffer.
BufWriter.finishfinish() throws(IOError | AllocError) -> ^WNone^W throws(IOError | AllocError)Flushes buffered bytes and returns the wrapped writer.
BufWriter.writewrite(data: bytes.Bytes) throws(IOError | AllocError)data: bytes to writevoid throws(IOError | AllocError)Buffers or forwards all bytes to the wrapped writer.

MemoryStream Constructors

NameSignatureParametersReturnsDescription
MemoryStreamMemoryStream()NoneMemoryStreamCreates an empty stream without allocating. Later growth uses the built-in allocation domain.
MemoryStreamMemoryStream(data: bytes.Bytes) throws(AllocError)data: bytes to copyMemoryStream throws(AllocError)Creates a stream containing a copy of the byte view.

MemoryStream Fields

FieldTypeDescription
lengthreadonly usizeExact buffered byte count.
capacityreadonly usizeLogical byte capacity of the owner.
positionreadonly usizeCurrent read cursor position.

All three are authoritative O(1), effect-free state. Test emptiness with stream.length == 0; compute unread bytes explicitly as stream.length - stream.position. There are no len, remaining, is_empty, or method-form measurement aliases.

MemoryStream Methods

MethodSignatureParametersReturnsDescription
readread(buffer: &[u8]) throws(IOError | AllocError) -> usizebuffer: destination bytesusize throws(IOError | AllocError)Copies bytes from the current cursor and advances the cursor.
writewrite(data: bytes.Bytes) throws(IOError | AllocError)data: bytes to appendvoid throws(IOError | AllocError)Appends bytes to the stream.
bytesstream.bytes()Nonebytes.BytesReturns a borrowed read-only view of all buffered bytes.
resetreset()NonevoidMoves the read cursor back to the beginning.
clearclear()NonevoidRemoves buffered bytes and resets the cursor.
reservereserve(capacity: usize) throws(IOError | AllocError)capacity: minimum byte capacityvoid throws(IOError | AllocError)Ensures the stream can hold at least capacity bytes.
shrinkshrink() throws(IOError | AllocError)Nonevoid throws(IOError | AllocError)Releases unused capacity when possible.

Status

std.io is the public sync I/O API. RFC 0054's OutputSpan<u8> overload and removal of the checked-in ByteBuffer are accepted but not yet implemented. RFC 0062's measurement fields, buffered-method rename, and removal of the old measurement helpers are also accepted target changes pending implementation. For async byte streams, see std.async.io. For the full exported surface, see Standard Library Inventory.

Released under the MIT License.