Follow-ups to the re-run flip, and all of one kind: each said something correct about a committed re-run that still read as parked, and the flip made it false. flan_merged_park's note that the program stays PROGRAM_PARKED across a poll is now true of every round but the one it leaves on — which is the round that drains the ring with a run already taken, and the round the fix's own argument leans on as the long one. Program.rerun's refusal told the reader to close a window or let the program finish. With a thunk stopped in the break loop a re-run is accepted, the state goes running, and a second is refused by that sentence — advice about a game loop for somebody whose thread is in a break. The C cannot see a break; this end can, so Dev.rerun passes what it already asked the agent and the refusal says resume or abort instead. The five-second timeout in eval_expr and run_render_thunk reached its break-loop sentence through the Parked arm, so a program stopped between runs was asked whether it calls (agent/poll). Both gain the arm that names the break. run_render_thunk's two sentences become a function first: the new one asks the agent, and that was the body of a five-millisecond tick. docs/BUILT.md gains the case an editor author meets — :parked nil :stopped t with no frames running — and FIX.org records that test_dev.ml:763's refusal assertion was racy before this and is narrowed by it.
104 lines
5.6 KiB
OCaml
104 lines
5.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.
|
|
|
|
[stopped] is the one thing the refusal cannot see for itself, and without it
|
|
the advice is wrong in the case that needs it most. The C knows the program
|
|
is not available; it does not know that the thread is sitting in a break
|
|
loop, which is a thing the agent reports and this end reads. "Close its
|
|
window, or let it finish" is what to do about a program that is running; it
|
|
is no use at all to somebody whose evaluation stopped on a breakpoint, or
|
|
who has already asked for a re-run that is waiting on that same break. Both
|
|
of those are ended by resuming or aborting, so that is what those are told.
|
|
The caller passes what it already knows — see [Dev.rerun]. *)
|
|
let rerun ?(stopped = false) () =
|
|
match raw_rerun () with
|
|
| 0 -> Ok ()
|
|
| 1 when stopped ->
|
|
Error
|
|
"the program is stopped at a break, so main cannot be started or \
|
|
restarted until that ends: resume it or abort it, and the program goes \
|
|
on from there. If a re-run was already accepted, it is waiting on the \
|
|
same thing and needs no second request"
|
|
| 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"
|