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.ioseparates byte capabilities from concrete storage and resources.io.readandio.writework 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
ByteBufferfrom the target. Owning^[u8]plus itsOutputSpan<u8>initialization session replace the duplicate growable-buffer abstraction and the unsoundstorage/set_lenpair. 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 historicalByteBuffer.appendno compatibility alias.MemoryStream.writeremainswrite: it implements the stream protocol even though this concrete writer places bytes at its end. RFC 0065 removesDescriptorReader,DescriptorWriter, andDescriptorStream. Safe portablestd.ioneither accepts nor returns an OS resource identity as an integer.
Standard Handles
stdin,stdout, andstderrare opaque process-lifetime values of nominalStdin,Stdout, andStderrtypes. They cannot be constructed, closed, or converted to a raw host identity.read_textandread_linevalidate bytes as Zynx text and throwstd.unicode.utf8.UnicodeError.Invalidonly for malformed UTF-8; U+0000 is preserved as ordinary counted text.print,println,eprint, andeprintlnwrite borrowed text to stdout or stderr and intentionally ignore low-level write status; they are convenience helpers for human-facing output, not a replacement forWriter.writewhen 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 assumetext.ptr[text.size] == 0. - Use explicitly owning string interpolation to prepare formatted text before passing it to those helpers. It requires
^, allocates, and reportsAllocError; see String Interpolation And Formatting for the syntax and format specifiers.
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);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.
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:
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.
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.
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.
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
| Name | Cases | Description |
|---|---|---|
IOError | WouldBlock, BrokenPipe, ConnectionAborted, Connection, ConnectionRefused, ConnectionReset, CrossDevice, DirectoryNotEmpty, FileExists, NotFound, Interrupted, IsADirectory, NotADirectory, Permission, Timeout, UnexpectedEof, Unsupported | Shared 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
| Type | Kind | Public fields | Methods | Description |
|---|---|---|---|---|
Reader | interface | N/A | read | Sync byte reader capability. |
Writer | interface | N/A | write | Sync byte writer capability. |
Stream | interface | N/A | read, write | Sync ordered byte stream capability. |
BufReader | struct: Reader | None | buffered_length, fill, read | Buffered reader over another sync reader. |
BufWriter | struct: Writer | None | buffered_length, into_inner, flush, finish, write | Buffered writer over another sync writer. |
Stdin | opaque process-lifetime Reader | None | read, read_text, read_line | Standard input with no constructor, raw identity, or close operation. |
Stdout | opaque process-lifetime Writer | None | write | Standard output with no constructor, raw identity, or close operation. |
Stderr | opaque process-lifetime Writer | None | write | Standard error with no constructor, raw identity, or close operation. |
MemoryStream | struct: Stream | readonly length, capacity, position | read, write, bytes, reset, clear, reserve, shrink | Owned in-memory stream backed by a growable byte buffer. |
Functions
| Name | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
print | print(text: str) -> void | text: borrowed text | void | Writes text to stdout and ignores low-level write status. |
println | println(text: str) -> void | text: borrowed text | void | Writes text and a trailing newline to stdout, ignoring low-level write status. |
eprint | eprint(text: str) -> void | text: borrowed text | void | Writes text to stderr and ignores low-level write status. |
eprintln | eprintln(text: str) -> void | text: borrowed text | void | Writes text and a trailing newline to stderr, ignoring low-level write status. |
stdin | stdin() -> Stdin | None | Stdin | Returns a process-lifetime non-owning standard-input view. |
stdout | stdout() -> Stdout | None | Stdout | Returns a process-lifetime non-owning standard-output view. |
stderr | stderr() -> Stderr | None | Stderr | Returns a process-lifetime non-owning standard-error view. |
write | write<W: Writer>(writer: &W, data: bytes.Bytes) throws(IOError | AllocError) | writer: sync writer, data: bytes to write | void throws(IOError | AllocError) | Writes all bytes or returns an error. |
read | read<R: Reader>(reader: &R, buffer: &[u8]) throws(IOError | AllocError) | reader: sync reader, buffer: mutable destination bytes | void throws(IOError | AllocError) | Fills the entire buffer or returns IOError.UnexpectedEof. |
Interface Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
Reader.read | read(&self, buffer: &[u8]) throws(IOError | AllocError) -> usize | buffer: destination bytes | usize throws(IOError | AllocError) | Reads bytes synchronously and returns the number of bytes read. |
Reader.read | read(&self, output: &OutputSpan<u8>) throws(IOError | AllocError) -> usize | output: bounded uninitialized byte suffix | usize throws(IOError | AllocError) | Initializes and advances exactly the returned prefix; an error advances zero bytes. |
Writer.write | write(self, data: bytes.Bytes) throws(IOError | AllocError) | data: bytes to write | void throws(IOError | AllocError) | Writes bytes synchronously or returns an error. |
Buffered Constructors
| Name | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
BufReader | BufReader<R: Reader>(source: R) | source: wrapped reader | BufReader<R> | Creates a buffered reader with the default capacity. |
BufReader | BufReader<R: Reader>(source: R, capacity: usize) | source: wrapped reader, capacity: buffer capacity | BufReader<R> | Creates a buffered reader with an explicit capacity. |
BufWriter | BufWriter<W: Writer>(target: W) | target: wrapped writer | BufWriter<W> | Creates a buffered writer with the default capacity. |
BufWriter | BufWriter<W: Writer>(target: W, capacity: usize) | target: wrapped writer, capacity: buffer capacity | BufWriter<W> | Creates a buffered writer with an explicit capacity. |
Buffered Methods
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
BufReader.buffered_length | buffered_length() -> usize | None | usize | Returns unread bytes already held in the buffer. |
BufReader.fill | fill() throws(IOError | AllocError) -> usize | None | usize throws(IOError | AllocError) | Refills the buffer from the wrapped reader and returns bytes read. |
BufReader.read | read(buffer: &[u8]) throws(IOError | AllocError) -> usize | buffer: destination bytes | usize throws(IOError | AllocError) | Reads bytes from the buffer and wrapped reader. |
BufWriter.buffered_length | buffered_length() -> usize | None | usize | Returns bytes waiting in the buffer. |
BufWriter.into_inner | into_inner() -> ^W | None | ^W | Returns the wrapped writer without flushing; buffered bytes are discarded. |
BufWriter.flush | flush() throws(IOError | AllocError) | None | void throws(IOError | AllocError) | Writes buffered bytes to the wrapped writer and clears the buffer. |
BufWriter.finish | finish() throws(IOError | AllocError) -> ^W | None | ^W throws(IOError | AllocError) | Flushes buffered bytes and returns the wrapped writer. |
BufWriter.write | write(data: bytes.Bytes) throws(IOError | AllocError) | data: bytes to write | void throws(IOError | AllocError) | Buffers or forwards all bytes to the wrapped writer. |
MemoryStream Constructors
| Name | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
MemoryStream | MemoryStream() | None | MemoryStream | Creates an empty stream without allocating. Later growth uses the built-in allocation domain. |
MemoryStream | MemoryStream(data: bytes.Bytes) throws(AllocError) | data: bytes to copy | MemoryStream throws(AllocError) | Creates a stream containing a copy of the byte view. |
MemoryStream Fields
| Field | Type | Description |
|---|---|---|
length | readonly usize | Exact buffered byte count. |
capacity | readonly usize | Logical byte capacity of the owner. |
position | readonly usize | Current 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
| Method | Signature | Parameters | Returns | Description |
|---|---|---|---|---|
read | read(buffer: &[u8]) throws(IOError | AllocError) -> usize | buffer: destination bytes | usize throws(IOError | AllocError) | Copies bytes from the current cursor and advances the cursor. |
write | write(data: bytes.Bytes) throws(IOError | AllocError) | data: bytes to append | void throws(IOError | AllocError) | Appends bytes to the stream. |
bytes | stream.bytes() | None | bytes.Bytes | Returns a borrowed read-only view of all buffered bytes. |
reset | reset() | None | void | Moves the read cursor back to the beginning. |
clear | clear() | None | void | Removes buffered bytes and resets the cursor. |
reserve | reserve(capacity: usize) throws(IOError | AllocError) | capacity: minimum byte capacity | void throws(IOError | AllocError) | Ensures the stream can hold at least capacity bytes. |
shrink | shrink() throws(IOError | AllocError) | None | void 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.