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

ExportTypeDescriptorDirection
stdinReadStream0read
stdoutWriteStream1write
stderrWriteStream2write

All three are exported con values, ready to use:

vyi
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:

vyi-error
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

yaml
fn (w: WriteStream) write(buf: [*]u8) WriteError!i32

Writes 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:

vyi-error
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

yaml
fn (r: ReadStream) read(buf: [*]u8) ReadError!i32

Fills buf from the stream and returns the number of bytes read. The contract:

  • read blocks 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:

vyi
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:

vyi
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:

yaml
enum ReadError  { Interrupted, BadDescriptor, Broken }
enum WriteError { Interrupted, BadDescriptor, NoSpace, Broken }
VariantOnMeaning@errmsg
Interruptedboththe operation was interrupted; retrying may succeed"read interrupted" / "write interrupted"
BadDescriptorboththe underlying descriptor is invalid"bad read descriptor" / "bad write descriptor"
NoSpacewritethe sink is full"no space on write stream"
Brokenboththe 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 maintry 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:

vyi
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

yaml
fn args() ArgList

args() 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

MemberSignatureDescription
lenfn (a: ArgList) len() i32number of arguments
atfn (a: ArgList) at(i: i32) [*]u8argument i as a byte view, for 0 <= i < len()
bufpub buf: [*]u8the backing buffer: all arguments, NUL-separated
countpub count: i32the 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.

vyi
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 rich Error type with construction-time stack capture

(guide 07).

  • @trace — source-location access for the live call stack.
  • @testingdescribe/it/expect for vyi test

(guide 16).

  • @crypto — secure randomness.
  • @c and @libc — the C type aliases and libc bindings

(guide 15).

  • @wasm and @wasi-preview1 — the raw WebAssembly/WASI surfaces @io itself

is built on; reach for them only when the portable surface here isn't enough.