Take a restart by its position, off a list that stopped moving

Two frames offering `retry` put both on the break loop's list and only the
inner one within reach: §4's walk takes the first frame offering a name, by
definition, so the outer clause was drawn, offered, and unreachable. The old
prompt showed `retry` twice and sent the string either way. An index is the
only thing that can say which one, which is why SBCL identifies them
positionally too.

An index is worthless against a stack that moves, though, and this one moves:
the break loop is the poll loop, so every restart-case an evaluation enters
pushes and pops the same global list between the listing and the choice. So
the list is read once on entry and copied — names into the agent's own buffer,
frames as the addresses a transfer carries — and every answer comes from that.
The name still travels with the index as a receipt, checked against the
snapshot and refused if the two have drifted, so a bare integer can be wrong
out loud.

And the third state. A restart below the thunk a break is inside was accepted,
announced, and silently not taken: `flan_reload_call` holds its own transfer
channel and drops it on return, so the unwind stops at the thunk. The boundary
is now recorded where it is made, at the call — frames a restart-case inside
the thunk pushes are above it and still work — and such a restart is listed,
marked, and refused with the reason.

`break.flan` grew the shadowed pair, and 900 is a value no by-name lookup in
that file can produce.
This commit is contained in:
Joseph Ferano 2026-09-12 04:56:18 +07:00
parent e416f28567
commit 4a6a8fa0f7
7 changed files with 452 additions and 55 deletions

View File

@ -195,18 +195,40 @@ let state t =
(* Innermost first, terminated by a line that is a single dot — the agent's
framing, not this one's. A refusal comes back as a line starting "err ", and
is passed on rather than turned into an empty list: no restarts and cannot
say are different answers. *)
say are different answers.
Each line is [I ± NAME]: the index it is taken by, whether it can be taken,
and the name. The index is the identity two frames may offer [retry] and
a name cannot say which and it is the program's number, not this end's
position in a list, so it is carried rather than recomputed. *)
let restarts t =
match ask t "restarts" with
| text ->
let lines = String.split_on_char '\n' text in
if List.exists (fun l -> String.length l >= 3 && String.sub l 0 3 = "err") lines
then Error (String.trim text)
else
else begin
let parse line =
match String.index_opt line ' ' with
| None -> None
| Some i ->
(match int_of_string_opt (String.sub line 0 i) with
| None -> None
| Some idx ->
let rest = String.sub line (i + 1) (String.length line - i - 1) in
if String.length rest < 2 then None
else
Some
( idx,
rest.[0] = '+',
String.sub rest 2 (String.length rest - 2) ))
in
Ok
(List.filter
(fun l -> l <> "" && l <> ".")
(List.map String.trim lines))
(List.filter_map parse
(List.filter
(fun l -> l <> "" && l <> ".")
(List.map String.trim lines)))
end
| exception Unix.Unix_error (e, _, _) -> Error (Unix.error_message e)
let alive t =
@ -465,7 +487,21 @@ let break t =
| Unreachable m -> error ("cannot ask the program whether it stopped: " ^ m)
| Stopped _ ->
(match restarts t with
| Ok names -> ok [ ":restarts " ^ Wire.strings names ]
| Ok rs ->
(* [:restarts] stays a list of names, positional and innermost first,
with duplicates kept the position *is* the index, which is what
[restart-at] takes. [:unreachable] names the positions that are on
the list and cannot be chosen: a restart below the evaluation the
break is inside has nowhere for a transfer to land. They are shown
rather than filtered, because a client that quietly dropped them
would leave someone asking where their restart went. *)
ok
[ ":restarts " ^ Wire.strings (List.map (fun (_, _, n) -> n) rs);
":unreachable "
^ Wire.ints
(List.filter_map
(fun (i, ok, _) -> if ok then None else Some i)
rs) ]
| Error m -> error ("the program refused to list its restarts: " ^ m))
(* A choice is validated by the *program*, on its listener thread, against a
@ -477,6 +513,30 @@ let break t =
stopped thread next comes round its loop, which is microseconds away and
still not now. An editor that read [ok] as "running again" would poll once,
find it stopped, and re-open the prompt it had just answered. *)
let choose_at t ~index ~name =
if not (alive t) then error "the program exited; restart flan dev"
else if
match name with
| Some n -> String.exists (fun c -> Char.code c < 32 || Char.code c = 127) n
| None -> false
then error "a restart name cannot contain a control character"
else
let verb =
"restart-at " ^ string_of_int index
^ match name with Some n -> " " ^ n | None -> ""
in
match ask t verb with
| reply when String.trim reply = "ok" ->
ok
[ ":index " ^ string_of_int index;
":note "
^ Wire.quote
"accepted; the program resumes at its next pass of the break loop"
]
| reply -> error (String.trim reply)
| exception Unix.Unix_error (e, _, _) ->
error ("cannot reach the program: " ^ Unix.error_message e)
let choose t ~name =
if not (alive t) then error "the program exited; restart flan dev"
else if String.exists (fun c -> Char.code c < 32 || Char.code c = 127) name then
@ -836,6 +896,15 @@ let handle t req =
(match Wire.string_field req "name" with
| Some name -> choose t ~name
| None -> error "restart needs :name")
(* By index, which is the one that can name a shadowed restart. [:name] is
optional and is not the lookup: it is checked against the name the program
has at that index and refused if they have drifted apart, so a client that
listed and then chose cannot take a different restart than the one it
showed. *)
| Some "restart-at" ->
(match Wire.int_field req "index" with
| Some index -> choose_at t ~index ~name:(Wire.string_field req "name")
| None -> error "restart-at needs :index")
| Some "abort" -> abort t
| Some "disassemble" ->
(match Wire.string_field req "name" with

View File

@ -89,6 +89,16 @@ let string_field form key =
| Some { Form.v = Form.Str s; _ } -> Some s
| _ -> None
(* A restart is chosen by index, so the protocol has to carry one. Kept as
narrow as [string_field]: a form that is not an integer is [None] and the
op says what it wanted, rather than this guessing at a string. *)
let int_field form key =
match field form key with
| Some { Form.v = Form.Int i; _ } -> Some (Int64.to_int i)
| _ -> None
let ints ns = list (List.map string_of_int ns)
let parse src =
match Reader.read_all ~file:"<wire>" src with
| [ f ] -> f

View File

@ -116,6 +116,29 @@ void *flan_find_restart(uint32_t name_id) {
return NULL;
}
/* The i'th frame itself, innermost first — the same walk as
* [flan_restart_name] and the other half of it. A break loop that offers
* someone a *list* has to be able to take the entry they picked, and §4's
* by-name lookup cannot express "the second retry": it takes the first frame
* offering the name, by definition, so a shadowed restart is on every list
* and reachable from none of them. Identifying a restart positionally is the
* only thing that fixes that, and it is why SBCL does the same.
*
* The address is the currency: a transfer carries the frame's address, so a
* caller that holds one from before is holding the same frame the walk found,
* whatever the walk finds today. */
void *flan_restart_frame(int32_t i) {
for (flan_restart *r = restarts; r != NULL; r = r->prev)
if (i-- == 0) return r;
return NULL;
}
/* Aim the transfer channel at a frame obtained earlier. The same store
* [flan_break_resume] makes and the same one an invoke-restart makes this
* only spells it without a lookup, for a caller that did its looking up when
* the stack was worth reading. */
void flan_restart_take(void *frame, void *xfer) { *(void **)xfer = frame; }
/* [T] and string are both ptr+len — see Emit.ll. */
typedef struct { const uint8_t *ptr; int64_t len; } flan_slice;

View File

@ -14,8 +14,21 @@
(use-placeholder [] -1)
(retry [] 7)))
;;; Two frames offering the same name, which §4 says resolves to the inner one
;;; and only ever the inner one. The outer clause is therefore on every list a
;;; break loop prints and reachable from no name at all, which is why a restart
;;; is taken by *index* now. 900 is the proof: it is the only value in this
;;; file that by-name lookup cannot produce.
(defn shadowed [n i32] i32
(restart-case
(+ (restart-case (do (error (Missing {:id n})) 0)
(retry [] 5))
100)
(retry [] 900)))
(defn main [] i32
(agent/start "/tmp/flan-break.sock")
(print-i64 (i64 (fetch 1))) (newline)
(print-i64 (i64 (fetch 2))) (newline)
(print-i64 (i64 (shadowed 3))) (newline)
0)

View File

@ -200,9 +200,9 @@ let () =
then fail "the program never reached the break loop: %S" !listed
else begin
(* Innermost first, and both on offer. *)
if !listed <> "retry\nuse-placeholder\n.\n" then
if !listed <> "0 + retry\n1 + use-placeholder\n.\n" then
fail "restarts on offer\n got: %S\n wanted: %S" !listed
"retry\nuse-placeholder\n.\n";
"0 + retry\n1 + use-placeholder\n.\n";
(* A name nothing offers is refused *here*, before the reply. Answering
ok and discovering it on the game thread would report success for
something that cannot happen. *)
@ -223,9 +223,41 @@ let () =
if not (await printed) then
fail "the first restart never produced its value"
else if not (await (fun () -> send bsock "restarts"
= "retry\nuse-placeholder\n.\n"))
= "0 + retry\n1 + use-placeholder\n.\n"))
then fail "the program never stopped a second time"
else ignore (send bsock "restart use-placeholder")
else begin
ignore (send bsock "restart use-placeholder");
(* -- Taking a restart by index ------------------------------- *)
(* The third break is inside a [restart-case] whose name the frame
below it also offers. Both are listed; §4's by-name walk can only
ever reach the first. So this takes the *second*, and 900 is a
value nothing else in the program can produce the assertion on
the output at the end is what makes this test about shadowing
rather than about a reply. *)
let printed2 () =
let t = In_channel.with_open_bin bout In_channel.input_all in
List.exists (String.equal "-1") (String.split_on_char '\n' t)
in
if not (await printed2) then
fail "the second restart never produced its value"
else if not (await (fun () -> send bsock "restarts"
= "0 + retry\n1 + retry\n.\n"))
then fail "the program never stopped on the shadowed pair"
else begin
(* Out of range is refused before the reply, like a bad name. *)
let oob = send bsock "restart-at 7" in
if not (String.length oob >= 3 && String.sub oob 0 3 = "err") then
fail "an index nothing offers was accepted: %S" oob;
(* The name rides along as a receipt, not as the lookup: an index
whose name has moved is refused rather than silently taken,
which is the same failure by-name lookup had. *)
let drift = send bsock "restart-at 1 use-placeholder" in
if not (String.length drift >= 3 && String.sub drift 0 3 = "err")
then fail "an index whose name had drifted was accepted: %S" drift;
ignore (send bsock "restart-at 1 retry")
end
end
end
end;
let bstatus = ref (Unix.WEXITED 0) in
@ -241,7 +273,7 @@ let () =
end
else begin
let text = In_channel.with_open_bin bout In_channel.input_all in
let want = "7\n-1\n" in
let want = "7\n-1\n900\n" in
let got =
String.concat "\n"
(List.filter

View File

@ -296,6 +296,73 @@ let () =
fail "installing while stopped: %s"
(Option.value ~default:"" (Wire.string_field r "message"));
(* -- A break inside a thunk, and the frames it cannot reach ---- *)
(* Evaluating here runs a thunk through [flan_reload_call], which holds
its own transfer channel and drops it on return. So a restart below
that C frame the two this program was already stopped on has
nowhere for a transfer to land: it would unwind to the thunk, stop,
and the program would carry on as though nothing had been chosen.
It used to be accepted and announced and silently not taken, which
is the worst of the three things that could happen.
Now the list says so and the choice is refused with the reason. *)
let r =
ask "(:op \"eval-expr\" :code \"(i64 (fetch 9))\" :file \"/tmp/buf.flan\")"
in
if status r <> "error" then
fail "an expression that stopped inside a break answered anyway";
let r = ask "(:op \"break\")" in
if status r <> "ok" then fail "break inside a thunk: %s" (status r);
(* Four now: the thunk's own [fetch] frame over the one below it. *)
let names =
match Wire.field r "restarts" with
| Some { Form.v = Form.List l; _ } ->
List.filter_map
(fun (n : Form.t) ->
match n.Form.v with Form.Str x -> Some x | _ -> None)
l
| _ -> []
in
if names <> [ "retry"; "use-placeholder"; "retry"; "use-placeholder" ]
then fail "restarts at a break inside a thunk: %s"
(String.concat ", " names);
let unreachable =
match Wire.field r "unreachable" with
| Some { Form.v = Form.List l; _ } ->
List.filter_map
(fun (n : Form.t) ->
match n.Form.v with
| Form.Int i -> Some (Int64.to_int i)
| _ -> None)
l
| _ -> []
in
if unreachable <> [ 2; 3 ] then
fail "positions below the thunk: %s"
(String.concat ", " (List.map string_of_int unreachable));
(* Refused, and refused *here* — not accepted and dropped. *)
let r = ask "(:op \"restart-at\" :index 2 :name \"retry\")" in
if status r <> "error" then
fail "a restart below the thunk boundary was accepted";
(* The ones above it still work, so this refuses a case rather than
disabling the feature. *)
let r = ask "(:op \"restart-at\" :index 0 :name \"retry\")" in
if status r <> "ok" then
fail "a restart inside the thunk was refused: %s"
(Option.value ~default:"" (Wire.string_field r "message"));
(* And the program is back on the *outer* break, which is the one it
was on before any of this the inner resume must not have been
read as the outer one resuming. *)
if not
(await (fun () ->
let r = ask "(:op \"break\")" in
status r = "ok"
&& (match Wire.field r "restarts" with
| Some { Form.v = Form.List l; _ } -> List.length l = 2
| _ -> false)))
then fail "the outer break did not come back after the inner one";
(* A name nothing offers is refused against the live stack, on the
program's listener thread, before the reply. *)
let r = ask "(:op \"restart\" :name \"nonesuch\")" in

View File

@ -103,6 +103,27 @@ extern int32_t flan_break_resume(const uint8_t *name, int64_t namelen,
void *xfer);
extern int32_t flan_restart_count(void);
extern const uint8_t *flan_restart_name(int32_t i, int64_t *len);
extern void *flan_restart_frame(int32_t i);
extern void flan_restart_take(void *frame, void *xfer);
/* -- How far down a transfer can actually land ----------------------- */
/* A thunk run from this loop is called through [flan_reload_call], which
* allocates its *own* transfer channel and drops it on return. So a restart
* chosen at a break inside a thunk unwinds as far as the thunk and no
* further: every frame below that C boundary is on the list, is accepted, and
* is silently not taken. Three statements that the program will resume, none
* of them true - which is worse than either resuming or refusing.
*
* The boundary is recorded where it is *made*, at the call, not at the break:
* the frames below it are exactly the ones already on the stack when the
* thunk started, and a restart-case the thunk enters itself is above it and
* reachable. Recording the depth on entering the break loop instead would
* count those too and refuse restarts that work.
*
* Game thread only, saved and restored around the call, so a thunk that
* breaks and whose break runs another thunk nests correctly. */
static int32_t restart_floor;
/* What the listener thread hands the stopped game thread. One slot, because
* only one thread is ever stopped. */
@ -116,9 +137,83 @@ static _Atomic int depth;
#define BREAK_MAX 8 /* deep enough to nest, shallow
* enough that a loop of breaks
* stops rather than grinds */
static char chosen[128];
static _Atomic int chosen_index;
static _Atomic int chosen_ready;
static _Atomic int aborting;
/* -- The snapshot ---------------------------------------------------- */
/* The stopped thread is not holding still. This loop runs [flan_agent_poll],
* which runs arbitrary Flan, and every restart-case that runs pushes and pops
* the one global restart list. Serving names straight off that list hands the
* listener a pointer into a frame that may already have been popped; serving
* *indices* off it is worse still, because an index carries no evidence of
* what it meant - the list and the choice can disagree and nothing can tell.
*
* So the list is read once, on entry, while the thread that owns it is in
* this function and not in Flan, and copied: names into a buffer of our own,
* frames as the addresses a transfer carries. Everything the listener answers
* comes from here and nothing re-reads the live stack. */
#define SNAP_MAX 64 /* restarts offered at one break */
#define SNAP_NAMES 4096 /* bytes of names behind them */
typedef struct {
int32_t n;
int32_t total; /* before SNAP_MAX truncated it */
void *frame[SNAP_MAX];
int32_t off[SNAP_MAX], len[SNAP_MAX];
int32_t reachable[SNAP_MAX];
int32_t used;
char names[SNAP_NAMES];
} snapshot;
/* One per nested break loop, because an inner break must not answer with the
* outer one's restarts and must not destroy them either - the outer loop is
* still going to need them when the inner one resumes. Same reason [depth] is
* a count and not a flag. */
static snapshot snaps[BREAK_MAX];
static _Atomic int snap_depth; /* published last; 0 = none */
static snapshot *snap_top(void) {
int d = atomic_load(&snap_depth);
return d <= 0 ? NULL : &snaps[d - 1];
}
/* Called on the game thread with the stack held still. 0 if there is no room
* to nest, which the caller reports rather than serving a stale one. */
static int snap_push(void) {
int d = atomic_load(&snap_depth);
if (d >= BREAK_MAX) return 0;
snapshot *s = &snaps[d];
int32_t n = flan_restart_count();
s->total = n;
s->used = 0;
s->n = 0;
for (int32_t i = 0; i < n && s->n < SNAP_MAX; i++) {
int64_t len = 0;
const uint8_t *nm = flan_restart_name(i, &len);
void *fr = flan_restart_frame(i);
if (nm == NULL || fr == NULL) continue;
if (len < 0) len = 0;
if ((int64_t)s->used + len + 1 > SNAP_NAMES) break;
s->frame[s->n] = fr;
s->off[s->n] = s->used;
s->len[s->n] = (int32_t)len;
/* The outermost [restart_floor] frames are below the thunk boundary. */
s->reachable[s->n] = (i < n - restart_floor);
memcpy(s->names + s->used, nm, (size_t)len);
s->used += (int32_t)len;
s->names[s->used++] = 0;
s->n++;
}
atomic_store(&snap_depth, d + 1);
return 1;
}
static void snap_pop(void) {
int d = atomic_load(&snap_depth);
if (d > 0) atomic_store(&snap_depth, d - 1);
}
/* The condition's class name, so an editor can say what stopped rather than
* only that something did. It is all there is to say: the hook is handed the
* name and an opaque pointer, and nothing at run time can render a value whose
@ -135,16 +230,30 @@ static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
fflush(stdout);
fprintf(stderr, "\nflan: unhandled %.*s — stopped, not dead.\n",
(int)namelen, (const char *)name);
/* Taken before anything is printed, and before [depth] says there is a
* break to ask about: the list on the terminal and the list on the socket
* are then the same list, numbered the same way, and the numbers are what a
* choice is made of. */
if (!snap_push()) {
fflush(stdout);
fprintf(stderr, "flan: %d nested break loops - giving up rather than "
"spinning\n", BREAK_MAX);
fflush(stderr);
exit(134);
}
{
int32_t n = flan_restart_count();
if (n == 0)
snapshot *s = snap_top();
if (s->n == 0)
fprintf(stderr, " no restarts are active; abort, or fix and reload\n");
for (int32_t i = 0; i < n; i++) {
int64_t len = 0;
const uint8_t *nm = flan_restart_name(i, &len);
if (nm != NULL)
fprintf(stderr, " restart: %.*s\n", (int)len, (const char *)nm);
}
for (int32_t i = 0; i < s->n; i++)
/* Numbered, because that is how one is taken now, and marked when it is
* not takeable - a restart below the thunk boundary is shown rather
* than hidden, since "why can I not have that one" is a fair question
* and silence is how this went wrong the first time. */
fprintf(stderr, " %2d. restart: %s%s\n", i, s->names + s->off[i],
s->reachable[i] ? "" : " (below this break; cannot be taken)");
if (s->total > s->n)
fprintf(stderr, " ... and %d more, not listed\n", s->total - s->n);
}
fflush(stderr);
/* What the *outer* loop was reporting, restored on the way out: resuming an
@ -183,13 +292,14 @@ static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
* arriving during the attempt passes its own check, writes a new name
* and sets the flag, and the store below then erases it. Copying also
* keeps strlen off a buffer the listener may be writing. */
char take[sizeof chosen];
memcpy(take, chosen, sizeof take);
int take = atomic_load(&chosen_index);
atomic_store(&chosen_ready, 0);
int32_t ok =
flan_break_resume((const uint8_t *)take, (int64_t)strlen(take), xfer);
snapshot *s = snap_top();
int ok = s != NULL && take >= 0 && take < s->n && s->reachable[take];
if (ok) {
fprintf(stderr, "flan: resuming at restart %s\n", take);
flan_restart_take(s->frame[take], xfer);
fprintf(stderr, "flan: resuming at restart %d. %s\n", take,
s->names + s->off[take]);
fflush(stderr);
memcpy(condition_name, outer_name, sizeof condition_name);
/* Cleared with the resume: an abort that passed its check just as the
@ -198,9 +308,13 @@ static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
* giving nobody the chance to choose. */
atomic_store(&aborting, 0);
atomic_fetch_sub(&depth, 1);
snap_pop();
return;
}
fprintf(stderr, "flan: no restart named %s is active\n", take);
/* The listener checks all of this before answering ok, so reaching here
* means the two disagreed - worth saying loudly rather than looping on
* in silence, which is the failure this whole change is about. */
fprintf(stderr, "flan: restart %d is not one this break can take\n", take);
fflush(stderr);
}
nanosleep(&step, NULL);
@ -224,8 +338,19 @@ int32_t flan_agent_poll(void) {
job j = queue[t % QUEUE];
atomic_store_explicit(&tail, t + 1, memory_order_relaxed);
if (j.install != NULL) { j.install(); n++; }
/* After the install, so a thunk sees the bodies its own module published. */
if (j.call != NULL) { j.call(); }
/* After the install, so a thunk sees the bodies its own module published.
*
* The floor moves for the duration. [flan_reload_call] holds its own
* transfer channel and drops it on return, so every restart frame that
* was on the stack before this call is unreachable from a break inside
* it. Saved and restored rather than set and cleared: this runs from
* inside break loops, which run from inside thunks. */
if (j.call != NULL) {
int32_t outer = restart_floor;
restart_floor = flan_restart_count();
j.call();
restart_floor = outer;
}
if (j.handle != NULL) { dlclose(j.handle); }
}
}
@ -307,43 +432,101 @@ static void serve(int fd) {
reply(fd, "running\n");
return;
}
/* One line per restart, innermost first: the index it is taken by, a flag
* for whether it can be taken at all, and the name. The index leads
* because it is the identity - two frames can offer [retry] and only one
* of them is the one meant, which is the whole reason this is not a list
* of names any more. Read from the snapshot, never from the live stack. */
if (strcmp(line, "restarts") == 0) {
if (!(atomic_load(&depth) > 0)) { reply(fd, "err not stopped\n"); return; }
int32_t n = flan_restart_count();
for (int32_t i = 0; i < n; i++) {
int64_t len = 0;
const uint8_t *nm = flan_restart_name(i, &len);
if (nm != NULL) {
send(fd, nm, (size_t)len, MSG_NOSIGNAL);
reply(fd, "\n");
}
snapshot *s = snap_top();
if (s == NULL) { reply(fd, "err no restart snapshot\n"); return; }
for (int32_t i = 0; i < s->n; i++) {
char hdr[32];
int k = snprintf(hdr, sizeof hdr, "%d %c ", i,
s->reachable[i] ? '+' : '-');
if (k > 0) send(fd, hdr, (size_t)k, MSG_NOSIGNAL);
send(fd, s->names + s->off[i], (size_t)s->len[i], MSG_NOSIGNAL);
reply(fd, "\n");
}
reply(fd, ".\n");
return;
}
if (strncmp(line, "restart ", 8) == 0) {
/* Take the i'th, optionally checking that the caller and this snapshot
* still agree on what the i'th is called. The name is not the lookup -
* that is the bug - it is a receipt: a client that listed, prompted, and
* chose has the name in hand for nothing, and sending it turns a bare
* integer into something that can be wrong out loud. */
if (strncmp(line, "restart-at ", 11) == 0) {
if (!(atomic_load(&depth) > 0)) { reply(fd, "err not stopped\n"); return; }
size_t k = strlen(line + 8);
if (k == 0 || k >= sizeof chosen) { reply(fd, "err bad restart name\n"); return; }
/* Checked here, against the stack the stopped thread is holding still,
* rather than accepted and found wrong after the reply has gone. "ok"
* has to mean the program will resume. */
{
int32_t n = flan_restart_count(), found = 0;
for (int32_t i = 0; i < n && !found; i++) {
int64_t len = 0;
const uint8_t *nm = flan_restart_name(i, &len);
found = nm != NULL && (size_t)len == k && memcmp(nm, line + 8, k) == 0;
}
if (!found) {
reply(fd, "err no restart named ");
reply(fd, line + 8);
reply(fd, " is active\n");
snapshot *s = snap_top();
if (s == NULL) { reply(fd, "err no restart snapshot\n"); return; }
char *end = NULL;
long idx = strtol(line + 11, &end, 10);
if (end == line + 11) { reply(fd, "err restart-at wants an index\n"); return; }
if (idx < 0 || idx >= s->n) {
reply(fd, "err no restart at that index\n");
return;
}
while (*end == ' ') end++;
if (*end != '\0') {
size_t k = strlen(end);
if (k != (size_t)s->len[idx] ||
memcmp(end, s->names + s->off[idx], k) != 0) {
reply(fd, "err that index is now ");
reply(fd, s->names + s->off[idx]);
reply(fd, ", not what you named; list the restarts again\n");
return;
}
}
memcpy(chosen, line + 8, k + 1);
/* Published last, so the game thread never reads a half-written name. */
if (!s->reachable[idx]) {
/* Refused, with the reason, rather than accepted and dropped. The
* transfer would unwind to the thunk this break is inside and stop
* there, and the program would carry on as if nothing had been
* chosen. */
reply(fd, "err restart ");
reply(fd, s->names + s->off[idx]);
reply(fd, " is below the evaluation this break is inside, so a "
"transfer to it has nowhere to land; choose one offered "
"above it, or abort\n");
return;
}
atomic_store(&chosen_index, (int)idx);
/* Published last, so the game thread never reads an index that is about
* to change. */
atomic_store(&chosen_ready, 1);
reply(fd, "ok\n");
return;
}
/* By name, still, for a person at a raw socket - and now defined as
* exactly [restart-at] on the first index offering the name, which is
* what §4's walk already meant. So the two verbs cannot disagree, and a
* shadowed name is reachable through the other one rather than nowhere. */
if (strncmp(line, "restart ", 8) == 0) {
if (!(atomic_load(&depth) > 0)) { reply(fd, "err not stopped\n"); return; }
snapshot *s = snap_top();
if (s == NULL) { reply(fd, "err no restart snapshot\n"); return; }
size_t k = strlen(line + 8);
if (k == 0) { reply(fd, "err bad restart name\n"); return; }
int32_t at = -1;
for (int32_t i = 0; i < s->n && at < 0; i++)
if ((size_t)s->len[i] == k && memcmp(s->names + s->off[i], line + 8, k) == 0)
at = i;
if (at < 0) {
reply(fd, "err no restart named ");
reply(fd, line + 8);
reply(fd, " is active\n");
return;
}
if (!s->reachable[at]) {
reply(fd, "err restart ");
reply(fd, line + 8);
reply(fd, " is below the evaluation this break is inside, so a "
"transfer to it has nowhere to land; choose one offered "
"above it, or abort\n");
return;
}
atomic_store(&chosen_index, at);
atomic_store(&chosen_ready, 1);
reply(fd, "ok\n");
return;