diff --git a/lib/dev.ml b/lib/dev.ml index 0cb1dfc..8a83abd 100644 --- a/lib/dev.ml +++ b/lib/dev.ml @@ -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 diff --git a/lib/wire.ml b/lib/wire.ml index d2d4b23..0a1d0ad 100644 --- a/lib/wire.ml +++ b/lib/wire.ml @@ -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:"" src with | [ f ] -> f diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c index cf932af..6de54a0 100644 --- a/runtime/flan_rt.c +++ b/runtime/flan_rt.c @@ -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; diff --git a/test/programs/break.flan b/test/programs/break.flan index d56ca52..12bc656 100644 --- a/test/programs/break.flan +++ b/test/programs/break.flan @@ -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) diff --git a/test/test_agent.ml b/test/test_agent.ml index 4f16c62..9627b1b 100644 --- a/test/test_agent.ml +++ b/test/test_agent.ml @@ -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 diff --git a/test/test_dev.ml b/test/test_dev.ml index 911e835..d87df25 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -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 diff --git a/vendor/agent/flan_agent.c b/vendor/agent/flan_agent.c index ad749d2..71fad9c 100644 --- a/vendor/agent/flan_agent.c +++ b/vendor/agent/flan_agent.c @@ -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;