The next release of GNU Guile, 2.0.10, is to be released “real soon
now”. My contribution to this release is the new (system repl coop-server)
module. This module introduces a useful variant of the
REPL server that I’ve named the “cooperative” REPL server. It is
cooperative because it can be integrated with single-threaded programs
without the thread synchronization issues present in the ordinary REPL
server.
Using delimited continuations and mvars (another new feature coming in Guile 2.0.10), it was possible to create a REPL server whose clients all ran in the context of a single thread. The cooperative server puts the user in control of when it is safe to evaluate expressions entered at the REPL prompt.
Here’s an example of how to use it:
(use-modules (system repl coop-server))
(define server (spawn-coop-repl-server))
(while #t
(poll-coop-repl-server server)
(sleep 1))
That’s all it takes. There are only 2 public procedures!
spawn-coop-repl-server
creates a new cooperative REPL server
object and starts listening for clients. poll-coop-repl-server
applies pending operations for the server.
Now that I’ve explained the API, onto the implementation details!
Although the REPLs run in the context of a single thread, there are
other threads needed to make it all work. To avoid thinking too much
about thread synchronization issues, I used the new (ice-9 mvars)
module to provide thread-safe objects for read/write operations. I
used delimited continuations in order to yield control back to the
user from inside the loop.
When the server is spawned, a new thread is created to listen for
client connections. When a client connects, a request to create a new
REPL is put into the server’s "evaluation" mvar, the storage place for
pending server operations. The job of poll-coop-repl-server
is to
attempt to take from the evaluation mvar and execute the operation
stored within, if any. In this case, an operation called new-repl
is hanging out along with the client socket. The contents of the mvar
are removed, and a new REPL is started in the main thread.
This new REPL is run within a "prompt", Guile’s implementation of
delimited continuations. The prompt allows the REPL to be paused
temporarily while waiting for user input. Just before the prompt is
aborted, a thunk containing the logic to read user input is stored
within the client’s "read" mvar for yet another thread to use.
Without this additional thread and the use of a prompt, the main
thread would block while waiting for input, defeating the purpose of
the cooperative REPL. The reader thread waits for this thunk to
appear in the read mvar. When it appears, the thunk is applied and
the resulting expression is stored as an eval
operation within the
evaluation mvar. When poll-coop-repl-server
is called, the REPL
prompt is resumed. The input expression is evaluated in the context
of the main thread, the result is printed, and the process repeats
itself.
That’s all, folks. Thanks for following along. I’m very excited to use the cooperative REPL server in guile-2d and I hope that others find it useful as well. Many thanks to Mark Weaver for helping me out when I got stuck and for all of the helpful code review.