1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
title: A Cooperative REPL Server for Guile 2.0.10
date: 2014-01-24 22:00
tags: gnu, scheme, guile, wsu
summary: Guile 2.0.10 will include a new type of REPL server suited for single-threaded applications.
---
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:
```scheme
(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.
|