Hi I'm Jan, a student from Berlin, Germany, and I'm planning to write my bachelor thesis about Roc.
More specifically about how the platform system may make it suitable for deterministic simulation testing.
This blog post by Phil Eaton is a good overview and introduction to what that is: https://notes.eatonphil.com/2024-08-20-deterministic-simulation-testing.html
I've not yet heard anyone talk about Roc and DST, except one guy for one minute on this podcast (https://shows.acast.com/dead-code/episodes/deterministic-simulation-with-stevan) 12:30-13:30.
Though I think Roc lends itself particularly well to DST, since it forces all of the nondeterministic operations to go through the platform layer (atleast the ones that I know of).
It is early days and nothing is decided yet, but my professor is open to it.
The thesis question could be something along the lines of this:
How can Roc's platform model be used to run one distributed system unchanged under deterministic simulation and in production, and what does this approach gain and lose compared with existing approaches?
The Plan:
I want to write a Raft implementation in pure Roc in the sans-io style (https://sans-io.readthedocs.io/how-to-sans-io.html) and on top of it a KV store.
I'll build two small platforms: one is a simulator and one has the real networking etc.
The platforms will be deliberately small and only include the functionality I actually need.
I did some testing and I think this is feasible. I don't need multithreading or async for this.
I would be glad if you could help me by answering a few questions:
I have a lot to learn, so I'm happy to hear your thoughts.
really cool! :smiley:
Jan Rüggeberg said:
- Did anyone already look into this domain (DST) in combination with Roc?
not as far as I know
- Do you think this is feasible at the current state of the language?
absolutely!
- I'm planning on writing the platforms in Rust, because I've only written about 200 lines of Zig so far. Is there a strong reason to go with Zig instead?
I'd say @Luke Boswell has the most experience with the differences in experience between writing Rust and Zig hosts, so I'll defer to him on that :smile:
Thanks a lot for the input!!
Hi Jan :)
Did anyone already look into this domain (DST) in combination with Roc?
I did an AI assisted search through our group chat and found no direct matches. Something adjacent is the simulation tests we plan for effectful functions by supplying simulated values, without running the real effects and without an effect-interpreter layer. So it's similar but does not use the DST randomness.
- Is there other nondeterministic behaviour in the language (like HashMap iteration order in Rust)?
no, we go way out of our way to make everything as deterministic as possible actually - including across targets, e.g.
Dict (our hashmap equivalent) in the sdlib always iterates in the order the keys were inserted, although under the hood it actually does some randomization of internal, private storage (using memory addresses as the seed so it can piggyback off the OS's ASLR instead of needing to do a syscall to get a random seed) to mitigate hash flooding attacks - but we've been careful to make sure this detail is not observable in Roc programs - you'd have to look at the raw bytes in memory (or observe performance characteristics, e.g. because you're attempting a hash flooding attack) to tell we're doing itusize or any other ways to write Roc code that gives different answers based on what target you're compiling for (see for example #beginners > ✔ OS-specific APIs in platforms) - the only thing that can happen differently on different targets is something crashing on one target but not on another, because it ran out of system resources (e.g. heap OOM, stack overflow, or if you have 64-bit addresses, you can make a List with more than 2^32 items in it, but if you try to do that on a 32-bit target, you'll get a crash when it runs out) the main reason we do this is actually because of our eager compile-time evaluation - which basically evaluates any expression at compile time if it can be evaluated at compile time (e.g. it doesn't depend on any runtime inputs). This is only safe to do if there is no chance that it would change the runtime behavior of the program, which in turn means (especially in conjunction with caching of compilation artifacts, which is critical to compilation speed) that evaluation on your machine has to give the same answer as evaluating on any other machine would, even if that other machine has a different CPU architecture, or is wasm, etc.
- Is there anything I glossed over or just forgot about?
I actually don't think you need to do sans-io for this. You can do something much cooler, namely taking an existing Roc platform like basic-cli and making a drop-in replacement for DST (or even forking that platform to add a DST "mode") where basically it just replaces every single I/O operation with DST equivalents instead of doing the actual I/O, and then running the program tells you the output of the simulation
I'll build two small platforms: one is a simulator and one has the real networking etc.
The platforms will be deliberately small and only include the functionality I actually need.
this should also work fine, although if you wanted to do them both in the same platform, that should work too :smile:
it sounds like a really nice fit for Roc!
I actually don't think you need to do sans-io for this. You can do something much cooler, namely taking an existing Roc platform like
basic-cliand making a drop-in replacement for DST (or even forking that platform to add a DST "mode") where basically it just replaces every single I/O operation with DST equivalents instead of doing the actual I/O, and then running the program tells you the output of the simulation
Yeah I think you are right, I've thought about the abstractions one level too high. Namely something like send! which would then go to the platform and the platform can decide if it wants to use a sim, HTTP, RPC etc. for it.
AFAIK TigerBeetle and FoundationDB use the send abstraction, but I think in Roc I can actually make the sim at the Tcp.* function layer.
I think this will remove the ability from the platform to decide what protocol it uses for the sending. It also means HTTP needs its own sim, since the HTTP calls are not going through the Roc Tcp layer. But it could be a good design decision and would make the thesis claim even stronger.
I will explore it deeper, but I also need to make sure that the scope of the thesis does not explode.
This should also work fine, although if you wanted to do them both in the same platform, that should work too :smile:
Do you see any strong upsides in putting them in the same one?
main advantage would be that if I'm writing an application, I can have a way to DST it without needing to switch platforms (and hope the semantics accurately match on the drop-in replacement)
I have a platform that does this already, if you take a look at roc-signals it swaps out the host depending on what your target it. The app runs identically and has no way to know the host is different. If you target the web you get a WASM module for loading into the browser. If you target native you have a test harness which is driven deterministically through scripts written in S-expressions.
Also have a look at roc-gui that has a nicer builtin test harness.
I experimented previously which completely capturing and replaying everything crossing the boundary in roc-ray but I ripped it out because it was overkill for graphics and memory requirements we large with any decent duration of capture... but the idea worked really well.
Re Rust verse Zig I dont think there is too much of a difference, both will be a good choice for your use cass.
See https://lukewilliamboswell.github.io/roc-signals/docs/testing/
https://lukewilliamboswell.github.io/roc-gui/#_testing_your_application
Thank you Luke, looks really cool!
I will definitely steal some stuff :)
I'll probably have some questions for you once I'm further into the project.
Last updated: Nov 04 2026 at 18:54 UTC