flan/lib/program.ml
Joseph Ferano b206e9a9a4 An expression evaluated against a parked program runs on the parked thread
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.
2026-09-18 23:11:12 +07:00

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"