A REPL you can fork
A REPL session accumulates definitions, values, and mutable objects as you work. We have extended SCI, a Clojure interpreter, so that a session can fork into independent histories. Each branch starts with the program's existing state and can evolve from there.
Fork one prompt into two live interpreters
The original REPL below has already defined a function, a Var, an atom, and a second name for that atom. Fork it, run the prepared mutation in the child, then return to the original. The child reports the new values; the original reports the earlier ones. Either history can fork again. The optional stress test retains 1,024 sibling interpreters for inspection.
The demo offers Superficie and Clojure syntax; both tabs operate on the same interpreter state.
Fork the language at any prompt.
Loading the interpreter…
Starting a forkable SCI runtime…
OPTIONAL STRESS TEST Create 1,024 live REPLs
Fork one initialized interpreter into 1,024 retained siblings. Every square in the matrix will be a real SCI runtime that can still evaluate the inherited program.
Open this test only when you want to allocate the additional runtimes.
- LIVE REPLS
- 0
- FORK TIME
- —
- THROUGHPUT
- —
- SAMPLED
- 0
- ISOLATION
- not checked
32 × 32. Each green square is one retained interpreter.
Choose a square or world number.Unchanged basisAnother unchanged forkHow live state branches
The function state was compiled before the fork. When either world calls it, label, counter, and alias resolve to that world's values. Both atom names still refer to one atom within each world, while its value can change independently in the parent and child. Later definitions, Var metadata, atom watches and validators, and volatiles follow the same model.
SCI creates the child by snapshotting its registered runtime cells and attaching them to a new evaluation context. The child continues from that state without rerunning earlier prompts.
SCI separates the identity of each Var or mutable primitive from its current state. Its stable handle selects state in the world where the function is running, so the child can change label while the parent continues to read :root.
label → slot 12 cells[12] = :root cells[12] = :experiment A lineage registry assigns integer slots to Vars and SCI-owned mutable objects. Each world realizes those slots in a dense array. When SCI analyses a Var read, it records the slot number at that read site; entering an evaluation installs the selected world's array. Descendant reads can therefore use an indexed slot path centred on cells[slot] instead of looking up the Var in the registry each time.
The atom named by counter and alias uses the same arrangement. Its handle carries the value-slot number directly, plus a separate slot for less frequently accessed metadata, its validator, and its watches. A mutation compares and sets only the selected atom's value slot, so concurrent changes to unrelated atoms do not make each other retry.
Keeping reads cheap
Every call to state reads Vars and dereferences the atom, while a fork happens only when we ask for another history. We expect that imbalance in applications too: many reads and mutations between comparatively rare forks. The representation puts the copying work at the fork boundary and keeps ordinary access inexpensive. A host selects it with :runtime-mode :forkable. SCI's default :standard runtime keeps its existing direct paths.
Forkable mode has two additional fast paths. The primary world continues to read Var roots directly. On its first fork, SCI materializes those roots into slots and keeps the two representations synchronized. Descendant worlds use the analysed slot reads described above. The selected array holder is cached in execution-local state, avoiding a walk through the context on each access.
When you fork the demo, SCI copies the logical portion of the source world's slot array. The browser worker serializes evaluation and fork requests. On the JVM, a fork first waits for active evaluations of that world to finish. The array holds references, so immutable and persistent values can remain shared while SCI-owned state and cooperating host values receive child realizations. Fork time and retained memory are proportional to the lineage's allocated logical slots, including slots left by short-lived objects. Spare backing-array capacity is not copied.
An O(1) branch could share a persistent tree or copy-on-write page table. That would move more bookkeeping onto later reads or mutations. A design with one persistent world root could also make unrelated state cells compete to replace that root. Dense arrays keep reads indexed and atom updates local to one slot. Page-level copy-on-write remains worth exploring for workloads with large worlds and frequent forks, where reducing the copy cost could justify the extra indirection.
Dynamic bindings and suspended work
The demo forks between prompts, after the previous evaluation has finished. A Clojure binding applies within an evaluation scope and unwinds when that scope exits, so its temporary values are already gone by the next prompt. Each branch can enter new scopes using nested binding and set!, with the usual unwinding behaviour.
A runtime such as Spindel can also suspend a computation inside those scopes and resume it in a child world. That requires the binding frames as well as the world's state. The experimental SCI API can capture an opaque continuation context, retarget it to another context in the same lineage, and invoke a continuation with the selected world installed.
These bindings live outside the dense world array. Each evaluation maintains persistent frame maps from Var identities to small mutable binding boxes. Entering a world selects the effective frame once, while ordinary non-dynamic Var reads bypass it. Retargeting copies the binding boxes for the child and preserves the frame chain needed to unwind nested scopes.
From the REPL to Simmis
We already use this implementation in Dvergr and the current Simmis prototype through our replikativ SCI build. The browser demo is pinned to a build from the same draft SCI pull request. The work is still experimental and has not been released in upstream SCI.
SCI handles the mutable state of its own primitives. If an application supplies other mutable objects, it needs to specify how their state should be copied or shared across a fork. A random-number generator, for example, needs a policy for the streams each branch will use.
External effects require a different kind of bookkeeping. Discarding a branch can release resources it owns, such as an open connection, but it cannot recall a request already sent to another service. The application has to account for those actions and their costs even when it discards the computation that produced them.
Follow how forkable computation becomes one component of controlled, branching work in Simmis.
simmis