Docs · Standard library
`@io`
@io is the process-I/O package: 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 [*]u8 byte views (pointer plus length; see
guide 09 — Pointers and memory) and
reports failure through ordinary error unions consumed with try and catch
(see guide 07 — Optionals and errors).
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: [*]u8) WriteError!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 returns a WriteError.
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(WriteError, i32) to i32
return Ok(0)
}read
fn (r: ReadStream) read(buf: [*]u8) ReadError!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.len
bytes. Call again for more.
Ok(0)means end of input, and only that. End of input is not an error;
ReadError is 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 enums, one per direction:
enum ReadError { Interrupted, BadDescriptor, Broken }
enum WriteError { Interrupted, BadDescriptor, NoSpace, Broken }| Variant | On | Meaning | @errmsg |
|---|---|---|---|
Interrupted | both | the operation was interrupted; retrying may succeed | "read interrupted" / "write interrupted" |
BadDescriptor | both | the underlying descriptor is invalid | "bad read descriptor" / "bad write descriptor" |
NoSpace | write | the sink is full | "no space on write stream" |
Broken | both | the stream is closed or failing (a broken pipe is the classic case) | "broken read stream" / "broken write stream" |
Both enums define @errmsg, so they
flow into inferred !T error channels and speak for themselves when an
unhandled failure reaches main — try stdin.read(buf) inside a fn main() !u8
just works, and a Broken that escapes prints error: broken read stream.
To handle stream failures locally instead, catch or switch as with any
error union:
import { stdout, stderr, WriteError } from "@io"
fn main() !u8 {
stdout.write("best effort\n") catch (e: WriteError) i32 => 0
switch stderr.write("to stderr\n") {
Ok(n): { }, // n bytes written
Err(e): {
switch e {
Interrupted: { },
BadDescriptor: { },
NoSpace: { },
Broken: { },
}
},
}
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 is 0.
- Heap-free. The arguments live in a static region the compiler reserves;
args() allocates nothing and every at(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) [*]u8 | argument i as a byte view, for 0 <= i < len() |
buf | pub buf: [*]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) and byte operations.
@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.