You can get most of this same behavior even without introducing new concepts.
Have your types be parametrized in a non-generic way with type parameters that come from other Zig modules, and then use the Zig build system to implement “generics” when composing an output executable (or a test one, or whatever artifact you’re generating).
In concrete (but a bit pseudozig) terms:
// Replica.zig
const StateMachine = @import("StateMachine");
const Storage = @import("Storage");
state: StateMachine,
storage: Storage,
// ...
// build.zig
const exe = b.addExecutable(.{
.name = "tigerbeetle"
// ...
});
exe.root_module.addImport("StateMachine", StateMachine);
exe.root_module.addImport("Storage", RealStorage);
const tests = b.addTest(...);
tests.root_module.addImport("StateMachine", StateMachine);
tests.root_module.addImport("Storage", TestStorage);
I’d be curious to know if you guys considered this approach at all and, if so, what issue you encountered with it that made you avoid it.