async runtime

revo runs fibers cooperatively. when a fiber hits i/o, it goes on to do more important things than waiting,
on other fibers. the scheduler picks another ready fiber. when i/o completes, the fiber wakes up

it’s important to me that your async code looks blocking but isn’t, because it makes simple scripts faster for free

this lets you spawn hundreds of fibers without threads, callbacks or other nasty things

fibers

a fiber is just a lightweight task the scheduler managws. you spawn them like this:

when s:send() would block, that fiber parks. the scheduler runs server fiber instead. once data is ready, the client fiber resumes with the result

concurrent networking for dummies

run it, open another terminal, type nc localhost 6767 and you’ll have yourself a shell
you can then open infinity more terminals and run the same command and have them be handled at once

the scheduler

the coolest thing here is that async ops operate on generic tokens

the scehduler, src/vm/scheduler.zig,

the key type is WaitEntry:

`fiber_id`:
    which fiber to wake
`wait_id`:
    file descriptor / socket handle
`intent`:
    read, write, or both
`token`:
    opaque state from the i/o layer
`on_ready`:
    callback when data arrives
`on_deinit`:
    cleanup

i/o polling

pollIoWaiters() in src/baselib/net.zig uses std.posix.poll() on posix. when a file descriptor is ready:

fn onRecvReady(vm: *VM, waiter: *Scheduler.WaitEntry, events: i16) !Scheduler.IoDispatchResult {
    const token = @as(*RecvWaitToken, @ptrFromInt(waiter.token));
    const rc = std.c.recv(waiter.wait_id, buffer.ptr, buffer.len, flags);
    
    deinitToken(RecvWaitToken, vm.runtime.alloc, waiter.token);
    waiter.token = 0;
    return try completeWaiter(vm, waiter, .ok, data);
}

the io driver

all socket io is readiness-driven, we dont do worker threads anymore

pollIoWaiters() in src/baselib/net.zig snapshots the waiter list under lock
(each waiter carries a generation stamp),
poll()s the snapshot with no locks held,
then revalidates every ready fd against the live list before dispatching.

ready entries are claimed (removed) before their callback runs,
so later appends, removals, or array growth can’t invalidate them;
still-pending entries go back with a fresh generation.

completions and callbacks run with the vm gil held

socket:send(data)

socket:recv(opts)

socket:accept()

extending to other i/o

to wait on a new fd source, park with a WaitEntry and handle it in a readiness callback:

see onRecvReady above and src/baselib/net.zig

performance

poll runs with zero timeout per scheduler cycle

recv buffers in stream.pending to handle partial reads. watch your allocation overhead. fibers allocate stack, so memory bounds your fiber count, not file descriptors

gotchas