Docs · Standard library
`@io`
@io is the process-I/O package: the Writer/Reader interfaces, the three
standard streams (stdin, stdout, stderr), and the program argument vector
(args). The surface is identical on every target — on wasm the streams speak
WASI, elsewhere libc — and the target split is resolved at compile time, so
only the active implementation exists in your binary.
Everything here works in terms of [*con]u8 / [*mut]u8 byte views (pointer plus length; see
guide 09 — Pointers and memory) and
reports failure through the open error channel consumed with try and catch
(see guide 07 — Optionals and errors).
The Writer and Reader interfaces
interface Writer { fn write(buf: [*con]u8) error!i32 }
interface Reader { fn read(buf: [*mut]u8) error!i32 }Writer is "a place bytes go", Reader "a place bytes come from". Both speak
the open error channel, so one signature covers every concrete sink or
source — each concrete raises its own error declarations, which coerce into
error at this boundary.
Code written against the interfaces takes any conforming value. Conformance is
structural and scope-witnessed: a type
satisfies Writer wherever a write with the right shape is in scope, and the
concrete boxes at the call site with &:
import { Writer, stdout, stderr } from "@io"
fn emit(w: Writer, msg: [*con]u8) error!i32 {
return w.write(msg)
}
fn main() !u8 {
try emit(&stdout, "out\n")
try emit(&stderr, "err\n")
return Ok(0)
}The standard streams below conform, and so does @mem's
Buffer (both directions) — write into a Buffer to
materialize bytes in memory instead of on a descriptor.
Streams
| Export | Type | Descriptor | Direction |
|---|---|---|---|
stdin | ReadStream | 0 | read |
stdout | WriteStream | 1 | write |
stderr | WriteStream | 2 | write |
All three are exported con values, ready to use:
import { stdout, stderr } from "@io"
fn main() !u8 {
try stdout.write("hello\n")
try stderr.write("something went wrong\n")
return Ok(0)
}The stream types are exported so you can name them in signatures, but their descriptor field is private to the package — the three exported values are the only stream instances there are. You cannot conjure a stream over an arbitrary descriptor:
import { WriteStream } from "@io"
fn main() !u8 {
con s: WriteStream = _{ fd: 1 } // error: field "fd" is private to its defining file
try s.write("x")
return Ok(0)
}write
fn (w: WriteStream) write(buf: [*con]u8) error!i32Writes all of buf to the stream. Short writes are handled internally —
write loops until every byte is delivered — so on success the returned count
always equals buf.len. On failure it raises a WriteError member
into the open error channel.
The result is an error union, not a bare count; consume it with try, catch,
or a switch:
import { stdout } from "@io"
fn main() !u8 {
con n: i32 = stdout.write("hi") // error: cannot assign Result(error, i32) to i32
return Ok(0)
}read
fn (r: ReadStream) read(buf: [*mut]u8) error!i32Fills buf from the stream and returns the number of bytes read. The contract:
readblocks until it can produce at least one byte.- A short read is normal — a successful read may return fewer than
buf.lenbytes. Call again for more. Ok(0)means end of input, and only that. End of input is not an error;ReadErroris reserved for genuine failures.
A single read:
import { stdin, stdout } from "@io"
import { systemAllocator } from "@mem"
fn main() !u8 {
con buf = try systemAllocator.alloc(64, 1)
con n = try stdin.read(buf) // blocks; Ok(0) only at end of input
if n > 0 {
try stdout.write(buf[..n]) // echo back what arrived
}
return Ok(0)
}Draining an entire input is the loop-until-Ok(0) pattern, reading into
successive slices of one buffer:
import { stdin, stdout } from "@io"
import { systemAllocator, ArenaAllocator } from "@mem"
fn main() !u8 {
mut arena = ArenaAllocator.new(systemAllocator)
con alloc = arena.allocator()
con buf = try alloc.alloc(512, 1)
defer alloc.free(buf)
mut filled = 0
for {
con n = try stdin.read(buf[filled..]) // Ok(0) = end of input
if n == 0 { break }
filled = filled + n
}
try stdout.write(buf[..filled])
return Ok(0)
}(Allocators are covered in the @mem reference.)
Errors
Two exported error declarations, one per
direction, each member carrying its intrinsic message:
error ReadError {
Interrupted "read interrupted";
BadDescriptor "bad read descriptor";
Broken "broken read stream";
}
error WriteError {
Interrupted "write interrupted";
BadDescriptor "bad write descriptor";
NoSpace "no space on write stream";
Broken "broken write stream";
}| Member | On | Meaning |
|---|---|---|
Interrupted | both | the operation was interrupted; retrying may succeed |
BadDescriptor | both | the underlying descriptor is invalid |
NoSpace | write | the sink is full |
Broken | both | the stream is closed or failing (a broken pipe is the classic case) |
The stream methods raise these members into the open error channel their
error!i32 signatures declare, so an unhandled failure that reaches a
fn main() !u8 speaks for itself — a Broken read that escapes prints
error: broken read stream.
To handle stream failures locally instead, catch the open error and
narrow by member path:
import { stdout, stderr, WriteError } from "@io"
fn main() !u8 {
stdout.write("best effort\n") catch (e: error) i32 => 0
switch stderr.write("to stderr\n") {
Ok(n: _): { }, // n bytes written
Err(e: _): {
switch e {
WriteError.Interrupted: { },
WriteError.BadDescriptor: { },
WriteError.NoSpace: { },
WriteError.Broken: { },
any: { },
}
},
}
return Ok(0)
}Program arguments
fn args() ArgListargs() returns the process argument vector as an ArgList. Two properties
define it:
- User arguments only. The list holds the arguments passed to the program; the program name is not included.
args().len()for a program run with no arguments is0. - Heap-free. The arguments live in a static region the compiler reserves;
args()allocates nothing and everyat(i)result is a view into that region, valid for the life of the program.
ArgList
| Member | Signature | Description |
|---|---|---|
len | fn (a: ArgList) len() i32 | number of arguments |
at | fn (a: ArgList) at(i: i32) [*con]u8 | argument i as a byte view, for 0 <= i < len() |
buf | pub buf: [*con]u8 | the backing buffer: all arguments, NUL-separated |
count | pub count: i32 | the same value len() returns |
at walks the shared buffer to the i-th argument and returns it without its
NUL terminator, so it costs O(buffer length) per call — fine for the handful of
arguments programs actually take. The pub fields expose the raw layout for
code that wants to scan the buffer itself.
import { args, stdout } from "@io"
fn main() !u8 {
con av = args()
mut i = 0
for i < av.len() {
try stdout.write(av.at(i))
try stdout.write("\n")
i = i + 1
}
return Ok(0)
}Note on other packages
@io covers the standard streams and arguments; the rest of the standard
library splits by concern:
@core— the auto-loaded prelude:Optional/Result,Vec,HashMap,String.@mem— allocators (systemAllocator,ArenaAllocator,FixedBufferAllocator), byte operations, andBuffer(an in-memoryWriter/Reader).@fmt— formatting: theIntoStringinterface andprint/println.@errors— the richErrortype with construction-time stack capture (guide 07).@trace— source-location access for the live call stack.@testing—describe/it/expectforvyi test(guide 16).@crypto— secure randomness.@cand@libc— the C type aliases and libc bindings (guide 15).@wasmand@wasi-preview1— the raw WebAssembly/WASI surfaces@ioitself is built on; reach for them only when the portable surface here isn't enough.