The park waited on one flag and could do one thing, so C-x C-e on (+ 1 1) was refused for want of a frame boundary — an expression that needs nothing from the program, in a process holding every global the run left. It waits on two now. A re-run leaves the park; a wake drains the agent's ring and waits again, with the state still PARKED, which is what makes running the thunk there exactly as safe as running it at a frame boundary: while parked there is no concurrency to be unsafe against. The break loop is the precedent and CL's spawned worker is deliberately not copied. A thunk that stops now stops on a parked thread, so the restart ops refuse on whether a break is engaged rather than on the state, and a paused expression against the park is resumable.
88 lines
4.6 KiB
OCaml
88 lines
4.6 KiB
OCaml
(** The Flan program's own thread, as the compiler reaches it when both are in
|
|
one process.
|
|
|
|
[flan dev] builds one binary that is the compiled program and holds this
|
|
compiler. The program owns the main thread; this compiler is a thread
|
|
beside it. What changed to make this module necessary is that a program
|
|
which finishes no longer ends the process — the main thread parks, keeping
|
|
every global the run left behind, and can be sent round [main] again. That
|
|
is the Common Lisp and Clojure arrangement and it is wanted for their
|
|
reason: closing a window should not cost the session, and getting another
|
|
one should not cost a rebuild.
|
|
|
|
Two facts cross from there to here, and both are a call into
|
|
[lib/dynload_stubs.c] rather than anything the daemon can infer. Liveness
|
|
used to be inferred — the program closed its stdout and the compiler read
|
|
EOF off the pipe — and that could not survive a program that runs again: a
|
|
pipe delivers EOF once, so the signal and the program's output were the
|
|
same resource. Asking is what replaces it.
|
|
|
|
[Absent] means there is no program thread in this process: the [flan]
|
|
binary that builds a merged program, [flan reload], the two-process daemon,
|
|
every test. As with [Agent.request] the answer comes from the linker — the
|
|
symbols are weak and null in a binary that links no merged entry point — so
|
|
it cannot disagree with reality the way a flag could. *)
|
|
|
|
type state =
|
|
| Running (* inside [main] *)
|
|
| Parked (* finished, waiting to be sent round again *)
|
|
| Absent (* this process has no program of its own *)
|
|
|
|
external raw_state : unit -> int = "flan_program_state"
|
|
external raw_rerun : unit -> int = "flan_program_rerun"
|
|
external raw_wake : unit -> int = "flan_program_wake"
|
|
|
|
let state () =
|
|
match raw_state () with 0 -> Running | 1 -> Parked | _ -> Absent
|
|
|
|
(* Ask a parked thread to drain the agent's ring, and keep it parked.
|
|
|
|
The second thing a park can service, and the one that makes an expression
|
|
evaluable against a finished program. A thunk is already in the ring by the
|
|
time this is called — it got there the way every module does — and what was
|
|
missing was anybody to pick it up: a parked thread reaches no frame boundary
|
|
and there was nothing else to wake it for.
|
|
|
|
Running it *there* is the whole design, and it is smaller than it sounds.
|
|
While parked the process has no concurrency at all — the program's thread is
|
|
asleep, no frame is executing, no global is being written — so the ring is
|
|
drained under exactly the conditions a frame boundary provides, on the
|
|
thread the program's own storage belongs to, with no second stack and no new
|
|
rule about where Flan code may run. Common Lisp answers the same question by
|
|
giving evaluation a thread of its own and calling the resulting race the
|
|
programmer's problem; this needs neither the thread nor the race.
|
|
|
|
[unit] and not a result, because there is nothing here for a caller to do
|
|
about either refusal. A running program polls its own ring at the next frame
|
|
boundary and a process with no program thread has no ring to drain; in both
|
|
the request has been satisfied by somebody else or was never this end's to
|
|
make. What a caller waits on is the value, which comes back the same way it
|
|
does from a frame boundary. *)
|
|
let wake () = ignore (raw_wake ())
|
|
|
|
(* Run [main] again, on the thread that ran it before.
|
|
|
|
The thread is not negotiable and is why there is no [Thread.create] here: a
|
|
window belongs to the thread that opened it, and on macOS to the first
|
|
thread of the process, so a second [main] anywhere else would draw nothing.
|
|
This only leaves a request and wakes the sleeper, which is the same rule the
|
|
agent already follows for everything the game thread has to do.
|
|
|
|
The refusal is the program's to make, not this end's. Checking the state
|
|
here and signalling after would leave a gap for the program to finish or
|
|
start in; the C does both under one lock, so a request is either taken or
|
|
told the program is running, and never both. *)
|
|
let rerun () =
|
|
match raw_rerun () with
|
|
| 0 -> Ok ()
|
|
| 1 ->
|
|
Error
|
|
"the program is already running; a re-run starts main again, and two \
|
|
mains in one process would be writing the same globals at once. Close \
|
|
its window, or let it finish, and ask again"
|
|
| _ ->
|
|
Error
|
|
"this session's program is a process of its own, so there is no parked \
|
|
thread here to send round again; it is the merged build that can re-run \
|
|
a program, not --two-process"
|