The break loop was reachable from a raw socket. This is the half that makes it reachable from an editor, and it all follows from one fact: a program stops at a moment nobody asked about. So the state is learned twice, on purpose. It rides on every reply, beside the program's output and for the same reason -- the likeliest instant for a program to stop is the one just after an evaluation, which is a reply the client is already reading, and learning it a second later from a poll would mean learning it after the echo area had said the evaluation was fine. And a timer asks anyway, once a second with `describe', because a program that stops in a frame of its own game loop produces no reply at all and folding state into replies that never come says nothing. The timer never reconnects -- that would quietly erase the `lost' state that exists to be seen -- and skips while a request is in flight, since accept-process-output runs timers and a poll firing inside a read would eat that read's reply. Three ops: `break' for the restart names, `restart' and `abort'. The annotation owns :stopped and :condition rather than the ops, so one place in the daemon decides whether the program is stopped and the poll and the prompt cannot disagree. "ok" from `restart' means accepted, not resumed: the choice is validated against the stopped stack and taken when that thread next comes round, so it says so and the client clears its own flag rather than polling once, finding it stopped, and re-opening the prompt it just answered. The agent grew one verb, `status', answered in both states. Everything else the break loop offers is refused while running, rightly; but the question an editor asks without already knowing had to have an answer either way or there would be nothing to poll. And flan_agent_poll had to become re-entrant, which was a bug rather than an addition. A C-x C-e thunk may itself error, and the break loop that catches it polls again from inside that call. The old loop cached both indices and stored tail at the end, rewinding over everything the nested poll consumed -- re-running the thunk that had just stopped the program, which is an unbounded recursion of breaks. Each job is now claimed before it is run. test_dev.ml evaluates an expression that errors and resumes it, which fails against the old shape.
34 lines
1.0 KiB
Plaintext
34 lines
1.0 KiB
Plaintext
;;;; A program that stops, for driving the break loop from an editor.
|
|
;;;;
|
|
;;;; It errors on its first frame, with nothing having handled the condition,
|
|
;;;; so [flan dev] meets a program that is already stopped — which is the state
|
|
;;;; an editor has to cope with, and the one that is hardest to arrange on
|
|
;;;; purpose later. Two restarts are on offer and they return different values,
|
|
;;;; so the transcript says which one was chosen.
|
|
;;;;
|
|
;;;; After resuming it keeps polling, because the claim worth testing is that
|
|
;;;; everything else still works on either side of a break.
|
|
(import agent "vendor:agent")
|
|
|
|
(defstruct Missing [id i32])
|
|
|
|
(defn fetch [n i32] i32
|
|
(restart-case
|
|
(do (error (Missing {:id n})) 0)
|
|
(use-placeholder [] -1)
|
|
(retry [] 7)))
|
|
|
|
(defvar ticks i64)
|
|
|
|
(defn step [] i64
|
|
(set ticks (+ ticks 1))
|
|
ticks)
|
|
|
|
(defn main [] i32
|
|
(agent/start "/tmp/flan-dev-break-fallback.sock")
|
|
(print-i64 (i64 (fetch 1))) (newline)
|
|
(dotimes [i 4000]
|
|
(agent/wait 5)
|
|
(set ticks (step)))
|
|
0)
|