Module: core/io

Violet’s async model uses an explicit *Io parameter rather than async/await keywords. Functions that take *Io may suspend; functions without it are provably synchronous.

Definitions

  interface Io {
    fn waitFd(*self, fd: Fd, event: IoEvent): !void;
    fn sleep(*self, dur: Duration): !void;
    fn yield_(*self): void;
    fn spawnBlocking(*self, alloc: Allocator, work: BoxedBlockingTask): !void;
    fn scope(*self, alloc: Allocator, body: BoxedScopeTask): !void;
    fn spawn(*self, alloc: Allocator, task: BoxedTask): !TaskHandle;
    fn spawnDetached(*self, alloc: Allocator, task: BoxedTask): !void;
    fn waker(*self): Waker;
    fn handle(*self, alloc: Allocator): IoHandle;
    fn run(*self): !void;
    fn shutdown(*self): void;
}

interface IoScope: Io {
    fn spawn(*self, alloc: Allocator, task: BoxedTask): !void;
}
  

Usage

  fn serveRequests(io: *Io, alloc: Allocator): !void {
    using alloc;

    try io.scope([](s: *IoScope) {
        while true {
            var conn = try acceptConnection(io);
            try s.spawn([move conn](_: *IoScope) {
                try handleConnection(io, conn);
            });
        }
    });
    -- all connection tasks joined before returning
}
  

Inside io.scope, use s for everything: IoScope extends Io, so s.waitFd, s.sleep, s.spawn all work through the scope.

Manual Task Management

  -- spawn with handle (must join or detach)
var handle = try io.spawn(alloc, [](s: *IoScope) {
    try doWork(io);
});
-- ... do other work ...
try handle.join();        -- wait for completion
-- or:
handle.detach();          -- convert to daemon task (cancelled on shutdown)

-- fire and forget daemon
try io.spawnDetached(alloc, [](s: *IoScope) {
    while true {
        try emitMetrics(io);
        try s.sleep(Duration::seconds(30));
    }
});
  

TaskHandle is linear — you must join, detach, or awaitCancelled. Dropping without one is a compile error (asserts in debug).

Cancellation

Tasks are cancelled cooperatively — error.Cancelled is returned at suspension points. errdefer handles cleanup:

  fn worker(io: *Io, alloc: Allocator): !void {
    var list = List[u8]::new();
    errdefer list.deinit();    -- runs if cancelled or any other error

    try io.waitFd(fd, .Read);  -- returns error.Cancelled if cancelled
    try process(&list);
    list.deinit();
}
  

CPU-Bound Work

  fn heavyWork(io: *Io, alloc: Allocator): !u64 {
    return try io.spawnBlocking(alloc, [](_: void): u64 {
        -- runs on thread pool, Violet task suspends
        return computeResult();
    });
}
  

Waker and Cross-Scheduler Communication

  type Waker = struct {
    ptr:    *anyopaque,
    vtable: *const WakerVTable,
};

-- Waker holds Weak reference to scheduler
-- wake() is a no-op if scheduler has been destroyed
  

#currentScheduler [TBD]

  fn downloadEasy(url: []u8): ![]u8 {
    var io = #currentScheduler();    -- thread-local escape hatch
    var conn = try tcp.connect(io, url);
    return try conn.readAll(alloc);
}
  

Use #currentScheduler only in application code, not library code. Libraries should take explicit *Io.