From 9b877a28354676c1f2f69655f6049250dc098a15 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Fri, 18 Sep 2026 23:16:35 +0700 Subject: [PATCH] A parked thunk's output, its empty globals section, and a better silence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- lib/dev.ml | 22 +++++++++++++++++++--- test/test_dev.ml | 49 ++++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/lib/dev.ml b/lib/dev.ml index cc1064e..9a46963 100644 --- a/lib/dev.ml +++ b/lib/dev.ml @@ -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 diff --git a/test/test_dev.ml b/test/test_dev.ml index 0decbde..936f0e2 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -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;