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