A parked thunk's output, its empty globals section, and a better silence

Three gaps in the first pass. The park flushes after the poll, which is the
only thing that gets a printing expression's line out of a fully buffered pipe
before the next run — now pinned by its position in the transcript. The globals
section under a parked break answers with everything skipped, which is a
different reply from the timeout a job nobody polled would give, so it is asked
for rather than assumed. And the render thunk's five-second sentence no longer
asks a parked program whether it is calling agent/poll.
This commit is contained in:
Joseph Ferano 2026-09-18 23:16:35 +07:00
parent b206e9a9a4
commit 9b877a2835
2 changed files with 64 additions and 7 deletions

View File

@ -1335,10 +1335,26 @@ let run_render_thunk ?(stopped_only = false) t ~tag ~(c : Session.change)
match resumed () with
| Some why -> Error why
| None ->
(* Two sentences for the same silence, because there are two
causes and each names a different thing to go and look at.
The frame-boundary one is about a game loop that is not
polling; said about a parked program it would be the sentence
this whole verb's parked refusal used to be, which is the one
thing not to say here. A parked thread has nothing competing
with it, so what it is doing is holding a break the thunk
asked for a stopped stack and the stop has been let go of. *)
let gave_up =
Error
"the program did not reach a frame boundary; is it calling \
(agent/poll)?"
if liveness t = Parked then
Error
"the inspection produced nothing in five seconds. The \
program is parked, so nothing is competing with the thunk: \
the break it was built against has most likely been \
resumed since, and a stopped-only job is dropped rather \
than run against a resumed program"
else
Error
"the program did not reach a frame boundary; is it calling \
(agent/poll)?"
in
if ms <= 0 then gave_up
else begin

View File

@ -564,6 +564,25 @@ let () =
fail "a global the finished run left: %s"
(Option.value ~default:(status r) (Wire.string_field r "message"));
(* What the thunk *printed*, which needs one more thing than the value
does. In the merged build fd 1 is a fully buffered pipe into the
daemon, and a run flushes at its own pace while the park flushes once
on the way in neither covers a thunk that printed after both, so
without a flush beside the poll this line would sit in the FILE buffer
until the next park, which is to say until after the next run. The
value arriving and the output not is exactly the shape that would go
unnoticed, so it is asked for by name. *)
let before = Buffer.length output in
let r =
request c
"(:op \"eval-expr\" :code \"(do (println \\\"pk\\\") 9)\" :file \"/tmp/buf.flan\")"
in
if Wire.string_field r "value" <> Some "9" then
fail "a printing expression against the park: %s"
(Option.value ~default:(status r) (Wire.string_field r "message"));
if not (contains_sub (Buffer.sub output before (Buffer.length output - before)) "pk")
then fail "a parked thunk's output never reached the daemon";
(* ── And a thunk that stops, on a thread with no run under it ──── *)
(* Parked and stopped at once, which is a pair of states that could not
@ -595,6 +614,18 @@ let () =
fail "a backtrace of a thunk stopped in the park: %s"
(Option.value ~default:(status r) (Wire.string_field r "message"));
(* And the section that walks that stack, which is the one op here whose
answer is *empty* and has to arrive anyway. Every frame a paused thunk
has is an eval frame, so nothing is attributed and everything lands in
[:skipped] but a render thunk is still built, delivered and run, and
"ok with nothing in it" is a different reply from the five-second
timeout that a job nobody polled would give. That is what this
distinguishes. *)
let r = request c "(:op \"globals\")" in
if status r <> "ok" then
fail "the globals of a thunk stopped in the park: %s"
(Option.value ~default:(status r) (Wire.string_field r "message"));
(* The way out, and the reason the restart ops had to stop refusing the
state: nothing else resumes this, and a re-run asked for meanwhile
would be taken and then wait on the same resume. *)
@ -656,8 +687,12 @@ let () =
106 rather than 1, because [extra] is a global of a process that never
died and the second run reads what the first left in it. Nothing is
zeroed between runs, deliberately: a clean slate is one evaluation
away, and cannot be had back once a re-run has wiped something. *)
if not (settle 6) then fail "the program did not run again";
away, and cannot be had back once a re-run has wiped something.
Seven and not six, because [settle] counts every line the daemon has
handed over and the printing expression above contributed one that no
run printed. Six would be satisfied by the first of these two. *)
if not (settle 7) then fail "the program did not run again";
(* And a re-run while it is running is refused rather than queued: two
mains in one process would be writing the same globals at once. *)
@ -686,10 +721,16 @@ let () =
again, from the body the first run ended with, and 106 from the one
delivered while it was parked. 106 and not 1 is the line that says
the globals are the finished run's the process never died, so
[extra] is where the first run left it. *)
[extra] is where the first run left it.
And [pk] between the two, which is a line no run printed: it is the
thunk evaluated against the park, on the parked thread, flushed there
rather than waiting for a run to flush it. Its position in the
transcript is the claim after everything the first run printed and
before anything the second did. *)
ignore (Unix.waitpid [] pid);
let text = Buffer.contents output in
let wanted = "1\n5\n105\n777\n777\n106\n" in
let wanted = "1\n5\n105\n777\npk\n777\n106\n" in
if text <> wanted then
fail "program transcript\n got: %S\n wanted: %S" text wanted
end;