I know I’m a little late but this is exactly what I have been building (Marionette), and I’ve got some notes that might be useful to the thread!
Short version: the “deterministic std.Io” idea works, at least for the storage/single-threaded part. I’ve been running an unmodified append-only DB (xitdb, which already takes an io: std.Io) on top of a deterministic Io backend over a simulated sector disk, with zero changes to its code, with crash/torn/lost-write fault injection and seed-reproducible replay. It found a recovery boundary (an 8-byte committed-size header that recovery assumes is written atomically, true on any 512/4096 disk, breaks only at sub-sector geometry, so not a real-hardware bug, but a precise atomicity assumption Marionette could pin down). The “run real code unmodified and reproduce from a seed” part is possible and actually useful. I also found a slew of bugs on a less mature database, which is obviously less interesting but good to know that it validates that this idea works.
The thing I didn’t expect and the most useful info I can offer is that a deterministic Io is necessary but not sufficient. You have to hunt uncertainty leaks (this is the same lesson madsim paid for, they had to intercept libc and patch dependency forks, because randomness and time escape through anything that bypasses runtime). The advantage in Zig is that std.Io is a way more comprehensive seam than anything Rust has, but it’s still not quite total. Anything a SUT or its deps reach for directly leaks nondeterminism past your Io and silently breaks replay. The cheap defense, which madsim/S2/FoundationDB all independently built is a meta-test that reruns the same seed and diffs the trace. If two same-seed runs diverge, something leaked.
@matklad’s point upthread is the one I’d underline hardest, because I backed into agreeing with it. Mocking at the Io layer is a great on-ramp, you get to run code that never heard of you. But the deep faults want the domain boundary, misdirecting a message is a more useful fault than corrupting a write, and it survives swapping the transport. So I’ve ended up at two tiers rather than one, Io-level for breadth/adoption, and a message-passing + sector-storage layer (à la TigerBeetle) for depth, and I’m specifically not throwing away the message layer in favor of mocking std.Io.net sockets.
@Cloudef got the concurrency answer, std.Io.fiber is reusable and the arch caveat barely matters for a CI/dev-machine simulator. The thing I’d add though is that the fiber primitive is the easy part and the deterministic scheduler on top is the work. And take Shuttle’s lesson over Loom’s, seeded randomized interleaving exploration, not exhaustive, because exhaustive is factorial and intractable, and randomized catches almost all non-adversarial bugs and parallelizes trivially (see the PCT paper, “A Randomized Scheduler with Probabilistic Guarantees of Finding Bugs”).
If anyone wants to see my work, you can check out Marionette (linked above) and its docs 