You run a program under flan dev, it opens a raylib window, you close the window, main returns — and there is no way to get another window short of flan-dev-restart-program, which throws away the build, the session and every global with it. In Common Lisp or Clojure the image outlives main, so you call it again. The process here already outlived main: the exit hook flushed, closed stdout and sat in for (;;) pause(). Nothing could wake it. So main() is a loop. The hook records the status and longjmps back into a setjmp in main() — there is no return available, since flan_exit is reached from wherever the program happened to be — and the thread waits on a condition variable until the new rerun op signals it. The main thread is the one that runs main again: a window belongs to the thread that opened it, and on macOS to the first thread of the process. A longjmp pops no frame, so the park first empties the handler stack, the restart stack and the shadow frame chain, each of which was a chain of allocas in stack the next run is about to write over. Nothing else is reset; the second run reads whatever the first left in the globals, which is the semantics that was asked for. Closing stdout had to go with it. That was how the compiler learned the program was done, but a pipe delivers EOF once, so the signal and the program's output were the same resource and spending it left the second run with nowhere to print. The descriptor hazard the old code reopened /dev/null for goes away with the close that caused it. Liveness is asked for instead, through a weak symbol in the same style as the agent's, and is now three states rather than two: Live, Parked and Gone. Every guard branches on that before consulting the break state, because the agent's listener answers "running" while the program is parked and telling somebody whose program has finished that it is running is worse than saying nothing. Only eval accepts a parked program — it queues and waits for nothing, and the queued module installs at the first frame boundary of the next run, so a body can be fixed while parked and the re-run executes it. Everything else needs a frame boundary or a stopped stack, has neither, and says which, naming the command that gets the program back. A re-run while the program is running is refused rather than queued: the test and the signal happen under one mutex, so two mains writing the same globals at once never starts. :parked rides on every reply beside :stopped, for the reason :stopped does — finishing is as unannounced as stopping, more so when the way it happens is a mouse click on a title bar. Emacs shows flan:parked in the modeline and binds flan-rerun to C-c C-M-x.
62 lines
3.0 KiB
OCaml
62 lines
3.0 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"
|
|
|
|
let state () =
|
|
match raw_state () with 0 -> Running | 1 -> Parked | _ -> Absent
|
|
|
|
(* 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"
|