Why are allocator interfaces and implementations split across `std.mem` / `std.heap`, while IO is colocated under `std.Io`?

The allocator interface lives at std.mem.Allocator, while allocator implementations live under std.heap, for example std.heap.ArenaAllocator. IO, on the other hand, appears more colocated under std.Io, for example std.Io.Threaded.

This means that using allocators requires hopping between std.mem and std.heap, while IO looks like it has a single entry point. From a discoverability and memorability perspective, this asymmetry stands out to me.

Current Zig shape:

const std = @import("std");
const Io = std.Io;
const Allocator = std.mem.Allocator;

pub fn main() void {
    var alloc_arena: std.heap.ArenaAllocator = .init(std.heap.page_allocator);
    defer alloc_arena.deinit();
    const alloc = alloc_arena.allocator();

    var io_threaded: std.Io.Threaded = .init(alloc);
    defer io_threaded.deinit();
    const io = io_threaded.io();

    _ = io;
}

Hypothetical shape:

const std = @import("std");
const Io = std.Io;
const Allocator = std.Allocator;

pub fn main() void {
    var alloc_arena: std.Allocator.ArenaAllocator = .init(std.heap.page_allocator);
    defer alloc_arena.deinit();
    const alloc = alloc_arena.allocator();

    var io_threaded: std.Io.Threaded = .init(alloc);
    defer io_threaded.deinit();
    const io = io_threaded.io();

    _ = io;
}

My question is mainly about the design rationale here. Is the difference with std.Io intentional, for example due to layering or taxonomy, or is it largely historical? Were alternative layouts considered or rejected for specific reasons such as naming, API stability, or module size?

I personally find std.heap.page_allocator under std.heap reasonable, since it is a low-level primitive. My question is mainly about the interface/implementation grouping and discoverability.

Related: “juicy main” was proposed as a way to reduce setup friction for newcomers, which is partly why I am asking about the structure here:
https://github.com/ziglang/zig/issues/24510

4 Likes

My guess is that it was historical. Moving std.mem.Allocator, or std.heap would be a pretty big change. There is an issue open about needing to audit the std lib before reaching 1.0, and I imagine that questions like this could be considered then.

9 Likes

I also have found std.mem.Allocator and std.heap.* to be an odd distinction, and I think std.Allocator and std.Allocator.* is a great move.

2 Likes

Agree. Allocators are usually given as a killer feature of Zig. They deserve prime API real estate.