Merge: all six traps park for inspection, two with the resume refused
This commit is contained in:
commit
e6d85c80dd
@ -139,10 +139,23 @@ The containers, strings/UTF-8, sequences, random, and printing layers are decent
|
||||
- **`abort()` in the dev runtime** — `flan_dev.c:47-50`, `:103`: reload-name-table
|
||||
exhaustion, intern OOM, and "global changed size" kill the game instead of signalling.
|
||||
Against the grain of everything else in the runtime; route through `flan_error`.
|
||||
- **Six trap paths bypass `flan_exit_hook`** and end a merged `flan dev` session
|
||||
(`flan_restart_fail`, `flan_restart_args_fail`, `flan_restart_unarmed`,
|
||||
`flan_transfer_fail`, `flan_null_alloc_fail`, `flan_free_all_fail` → `rt_die`). Bounds
|
||||
and arithmetic already park via the break-loop hook; these should too.
|
||||
- ~~**Six trap paths bypass `flan_exit_hook`** and end a merged `flan dev` session~~ —
|
||||
fixed 2026-09-18. Two corrections to the entry as written. The hook bounds and
|
||||
arithmetic park through is `flan_break_hook`, not `flan_exit_hook` — that second one is
|
||||
normal termination, the one `main` reaches when it ends. And the six could not be routed
|
||||
through it as it stands: `flan_break_hook`'s contract is that the loop may answer by
|
||||
aiming a transfer channel, and these six are called by emitted code that falls off the
|
||||
end with no channel in the call at all, so a restart chosen against one would be accepted
|
||||
and silently dropped. So there is a second hook, `flan_trap_hook`, and a break that
|
||||
refuses the resume with a reason rather than a process that exits before anyone can ask
|
||||
a question. All six park, for two reasons rather than one. Four are guards that fire
|
||||
*before* the operation they guard (`if (!a)` and the capability test both precede
|
||||
`a->proc(...)`), so nothing is half done. The other two — `flan_transfer_fail` and
|
||||
`flan_restart_unarmed` — fire mid-transfer, with this frame's defers possibly half run,
|
||||
and park only to be *looked at*: stopping on a torn unwind is strictly more than exiting
|
||||
before anyone can ask what tore it. Standalone is unchanged: the
|
||||
hook is null in a program that did not import the agent, and the trap still exits 134
|
||||
with the same sentence.
|
||||
- **Unchecked `malloc` in `flan_argv`** (`flan_rt.c:162`) — the one in the file.
|
||||
- ~~**`v->gen` stale-slice word is maintained and never consulted**~~ — deleted
|
||||
2026-09-18 with the second round of the repeal; the header is five words now.
|
||||
|
||||
@ -527,6 +527,52 @@ _Noreturn void flan_bounds_fail(const uint8_t *loc, int64_t loclen,
|
||||
void (*flan_break_hook)(const uint8_t *name, int64_t namelen, void *condition,
|
||||
void *xfer);
|
||||
|
||||
/* The other half of that, for the traps that have nowhere to resume *to*.
|
||||
*
|
||||
* Six refusals in this file are reached with no transfer channel in their
|
||||
* hands: the emitted code calls them and falls off the end, because the
|
||||
* checker has already decided the program is not going to continue. Under a
|
||||
* merged `flan dev' that decision was being made on the program's behalf and
|
||||
* charged to the *session*, since rt_die is [_exit] and the compiler is in the
|
||||
* same process — a program that named a restart nobody established took the
|
||||
* daemon down with it, which is exactly what the break loop exists to stop.
|
||||
*
|
||||
* So they park instead. Not with [flan_break_hook]: that hook's contract is
|
||||
* that the loop may answer by aiming the channel, and there is no channel
|
||||
* here — a restart chosen against one of these would be a transfer nothing
|
||||
* carries out, which is the "accepted and silently dropped" shape the
|
||||
* thunk-boundary refusal in flan_agent.c already went to some length to avoid.
|
||||
* This hook says the other thing: stop here, let everything be read, and
|
||||
* refuse a resume with a reason when one is asked for. The loop behind it
|
||||
* never returns; if some future one does, the [rt_die] below is still the
|
||||
* answer, which is also what a standalone build does every time, because
|
||||
* nothing installs this outside a dev session.
|
||||
*
|
||||
* All six park, and they do not all park for the same reason. Four are guards
|
||||
* that fire *before* the thing they guard — the null and capability tests
|
||||
* above [a->proc], and the two restart lookups — so nothing is half done and
|
||||
* the frame is as readable as any other. The other two are not: a transfer is
|
||||
* already under way at [flan_transfer_fail] and at [flan_restart_unarmed],
|
||||
* and this frame's defers may be half run, which is what the note on
|
||||
* flan_transfer_fail says a few lines down. Those two park anyway, and only
|
||||
* to be *looked at*: a half-run unwind is a thing worth seeing, and the
|
||||
* resume is refused there for the same reason it is refused at the other
|
||||
* four. Stopping on a torn transfer is strictly more than exiting before
|
||||
* anyone could ask what tore it.
|
||||
*
|
||||
* The name is a trap's name and not a condition's — there is no defstruct
|
||||
* behind [NullAllocator], and the `layout' op will say it cannot place it.
|
||||
* That is already a case the conditions buffer draws (see flan-cnr.el, which
|
||||
* treats a refusal as a reason to show rather than an error): the name is
|
||||
* there to say *which* trap the program is standing in, and the sentence each
|
||||
* site printed just above carries the detail. */
|
||||
void (*flan_trap_hook)(const uint8_t *name, int64_t namelen);
|
||||
|
||||
static _Noreturn void rt_trap(const uint8_t *name, int64_t namelen) {
|
||||
if (flan_trap_hook != NULL) flan_trap_hook(name, namelen);
|
||||
rt_die();
|
||||
}
|
||||
|
||||
/* Must agree with Check.type_id, byte for byte, or a name typed at the break
|
||||
* loop matches nothing. FNV-1a over the name, 32 bits. */
|
||||
static uint32_t flan_name_id(const uint8_t *s, int64_t n) {
|
||||
@ -566,13 +612,15 @@ void flan_error(uint32_t type_id, void *condition, void *xfer,
|
||||
|
||||
/* Nothing on the restart stack offers the name. It is reported where the
|
||||
* invoke was, because that is the only place that knows what was asked for;
|
||||
* there is nowhere to resume, so there is nothing else to do. */
|
||||
* there is nowhere to resume *to*, which is not the same as there being
|
||||
* nothing else to do — see rt_trap, which stops here instead of ending the
|
||||
* process, so that the stack that offered no such name can be read. */
|
||||
_Noreturn void flan_restart_fail(const uint8_t *loc, int64_t loclen,
|
||||
const uint8_t *name, int64_t namelen) {
|
||||
rt_flush_out();
|
||||
fprintf(stderr, "%.*s: no restart named %.*s is active\n",
|
||||
(int)loclen, (const char *)loc, (int)namelen, (const char *)name);
|
||||
rt_die();
|
||||
rt_trap((const uint8_t *)"NoSuchRestart", 13);
|
||||
}
|
||||
|
||||
/* The frame the name found does not take these arguments — spec-conditions.md
|
||||
@ -588,7 +636,7 @@ _Noreturn void flan_restart_args_fail(const uint8_t *loc, int64_t loclen,
|
||||
fprintf(stderr, "%.*s: restart %.*s takes %.*s, given %.*s\n",
|
||||
(int)loclen, (const char *)loc, (int)namelen, (const char *)name,
|
||||
(int)wantlen, (const char *)want, (int)gotlen, (const char *)got);
|
||||
rt_die();
|
||||
rt_trap((const uint8_t *)"RestartArity", 12);
|
||||
}
|
||||
|
||||
/* A clause with parameters was reached by a transfer that filled none of them
|
||||
@ -606,7 +654,7 @@ _Noreturn void flan_restart_unarmed(const uint8_t *loc, int64_t loclen,
|
||||
"break loop yet\n",
|
||||
(int)loclen, (const char *)loc, (int)namelen, (const char *)name,
|
||||
(int)wantlen, (const char *)want);
|
||||
rt_die();
|
||||
rt_trap((const uint8_t *)"RestartUnarmed", 14);
|
||||
}
|
||||
|
||||
/* Something a defer called invoked a restart. A defer is the cleanup a
|
||||
@ -620,7 +668,7 @@ _Noreturn void flan_transfer_fail(const uint8_t *loc, int64_t loclen) {
|
||||
"%.*s: a defer invoked a restart, which a defer may not do — it is "
|
||||
"the cleanup a transfer runs on its way out\n",
|
||||
(int)loclen, (const char *)loc);
|
||||
rt_die();
|
||||
rt_trap((const uint8_t *)"TransferFromDefer", 17);
|
||||
}
|
||||
|
||||
_Noreturn void flan_slice_fail(const uint8_t *loc, int64_t loclen,
|
||||
@ -1311,7 +1359,7 @@ _Noreturn void flan_null_alloc_fail(const uint8_t *loc, int64_t loclen) {
|
||||
"%.*s: this allocator is null — a zeroed Allocator was never given "
|
||||
"one\n",
|
||||
(int)loclen, (const char *)loc);
|
||||
rt_die();
|
||||
rt_trap((const uint8_t *)"NullAllocator", 13);
|
||||
}
|
||||
|
||||
_Noreturn void flan_free_all_fail(const uint8_t *loc, int64_t loclen) {
|
||||
@ -1321,7 +1369,7 @@ _Noreturn void flan_free_all_fail(const uint8_t *loc, int64_t loclen) {
|
||||
"release, and releasing nothing is not the same as releasing "
|
||||
"everything\n",
|
||||
(int)loclen, (const char *)loc);
|
||||
rt_die();
|
||||
rt_trap((const uint8_t *)"NoFreeAll", 9);
|
||||
}
|
||||
|
||||
/* ── The region requirement, spec-memory.md's arena rule ───────────────
|
||||
|
||||
34
test/programs/dev-trap-free-all.flan
Normal file
34
test/programs/dev-trap-free-all.flan
Normal file
@ -0,0 +1,34 @@
|
||||
;;;; A program that stops on a trap it cannot be resumed from, for driving the
|
||||
;;;; break loop over one.
|
||||
;;;;
|
||||
;;;; dev-break-bounds.flan is the case where stopping and *resuming* both work:
|
||||
;;;; a bad index signals BoundsError, the walk finds nothing, and the break
|
||||
;;;; loop hands the program back to its own `continue`. This is the other half.
|
||||
;;;; `free-all` on an allocator that has no region to release is a refusal the
|
||||
;;;; emitted code makes no channel for — it calls the trap and falls off the
|
||||
;;;; end — so there is nothing for a chosen restart to transfer into, and until
|
||||
;;;; now that meant `_exit(134)`, which under a merged `flan dev` is the
|
||||
;;;; session and the compiler as well as the program.
|
||||
;;;;
|
||||
;;;; The claim is that the session survives it anyway. The program stops where
|
||||
;;;; it erred, everything is readable, and the *resume* is the only thing
|
||||
;;;; refused — with a sentence saying why, rather than by the process being
|
||||
;;;; gone before anyone could ask.
|
||||
;;;;
|
||||
;;;; The `restart-case` is load-bearing and not scenery: with no restart on the
|
||||
;;;; stack the break loop would say "no restarts are active" and the refusal
|
||||
;;;; that had to be written would never run. `continue` is live, is listed, and
|
||||
;;;; is still not takeable — which is exactly the state this trap leaves a
|
||||
;;;; program in.
|
||||
(import agent "vendor:agent")
|
||||
|
||||
(defn release [] ()
|
||||
;; The heap allocator frees one block and owns no region, so this traps.
|
||||
(free-all (heap-allocator)))
|
||||
|
||||
(defn main [] i32
|
||||
(agent/start "/tmp/flan-dev-trap-free-all-fallback.sock")
|
||||
(restart-case
|
||||
(release)
|
||||
(continue [] (println "resumed")))
|
||||
0)
|
||||
18
test/programs/dev-trap-null-alloc.flan
Normal file
18
test/programs/dev-trap-null-alloc.flan
Normal file
@ -0,0 +1,18 @@
|
||||
;;;; The same claim as dev-trap-free-all.flan over the other allocator trap,
|
||||
;;;; and with nothing on the restart stack.
|
||||
;;;;
|
||||
;;;; `nowhere` is a zeroed `Allocator` — a `defvar` nobody assigned — and
|
||||
;;;; `free-all` on it is the "I released the region" / "I never made one"
|
||||
;;;; collapse the runtime refuses to let a program spell the same way. The
|
||||
;;;; refusal is a trap with no transfer channel, so it parks rather than
|
||||
;;;; resumes, and here there is not even a restart to list: the break loop says
|
||||
;;;; so and stands still, which is the whole difference between a session you
|
||||
;;;; can fix the program in and a daemon that is gone.
|
||||
(import agent "vendor:agent")
|
||||
|
||||
(defvar nowhere Allocator)
|
||||
|
||||
(defn main [] i32
|
||||
(agent/start "/tmp/flan-dev-trap-null-alloc-fallback.sock")
|
||||
(free-all nowhere)
|
||||
0)
|
||||
149
test/test_dev.ml
149
test/test_dev.ml
@ -1120,6 +1120,155 @@ let () =
|
||||
end
|
||||
end;
|
||||
|
||||
(* ── A break over a trap with no way back ─────────────── *)
|
||||
|
||||
(* The block above stops on a bad index and *resumes*: the signal walks the
|
||||
handlers, reaches the break loop, and the program's own [continue]
|
||||
carries it out. Six refusals in the runtime cannot be reached that way.
|
||||
[flan_restart_fail], [flan_restart_args_fail], [flan_restart_unarmed],
|
||||
[flan_transfer_fail], [flan_null_alloc_fail] and [flan_free_all_fail]
|
||||
are called by emitted code that then falls off the end, with no transfer
|
||||
channel anywhere in the call — so there is nothing for a chosen restart
|
||||
to land in, and every one of them called [_exit(134)]. In a merged
|
||||
[flan dev] that is one process, and the session went with the program.
|
||||
|
||||
Two of the six are driven here, and the pair is chosen so that the
|
||||
refusal has something to refuse: [dev-trap-free-all.flan] traps inside a
|
||||
live [restart-case], so [continue] is on the stack, is listed, and is
|
||||
still not takeable; [dev-trap-null-alloc.flan] traps with an empty
|
||||
restart stack, which is the other shape the break loop has to print.
|
||||
|
||||
What is asserted is the difference the change is about. The program is
|
||||
*stopped* and not gone — [describe] answers, and answers [:stopped] — an
|
||||
expression still evaluates while it stands there, which is the proof the
|
||||
daemon is alive and serving, and a resume is refused *with a reason*
|
||||
rather than by the process having exited before anyone could ask. The
|
||||
standalone half of the same claim is test_acceptance.ml's
|
||||
free-all-refused, which still exits 134: nothing installs the hook in a
|
||||
program that did not import the agent. *)
|
||||
let trap_park what prog cond restarts =
|
||||
let tsock = tmp (prog ^ ".sock") and tout = tmp (prog ^ ".out") in
|
||||
(try Sys.remove tsock with Sys_error _ -> ());
|
||||
let tfd =
|
||||
Unix.openfile tout [ Unix.O_WRONLY; Unix.O_CREAT; Unix.O_TRUNC ] 0o600
|
||||
in
|
||||
let tpid =
|
||||
Unix.create_process flan
|
||||
[| flan; "dev"; "programs/" ^ prog; "-s"; tsock |]
|
||||
Unix.stdin tfd Unix.stderr
|
||||
in
|
||||
Unix.close tfd;
|
||||
if not (listening ~pid:tpid tsock) then begin
|
||||
fail "the %s trap daemon %s" what !listen_why;
|
||||
(try Unix.kill tpid Sys.sigkill with Unix.Unix_error _ -> ())
|
||||
end
|
||||
else begin
|
||||
let c = connect tsock in
|
||||
let ask sexp = Wire.parse (Wire.send c sexp; Wire.recv c) in
|
||||
let stopped r =
|
||||
match Wire.field r "stopped" with
|
||||
| Some { Form.v = Form.Sym "t"; _ } -> true
|
||||
| _ -> false
|
||||
in
|
||||
let last = ref (ask "(:op \"describe\")") in
|
||||
if not
|
||||
(await (fun () -> last := ask "(:op \"describe\")"; stopped !last))
|
||||
then fail "the %s trap never stopped the program, it ended it" what
|
||||
else begin
|
||||
(* Which trap, by name. There is no [defstruct] behind these — they
|
||||
are traps and not conditions, and [layout] will say it cannot
|
||||
place the name, which flan-cnr.el already draws as a reason. The
|
||||
name is here to say where the program is standing. *)
|
||||
let got =
|
||||
match Wire.string_field !last "condition" with
|
||||
| Some c -> c
|
||||
| None -> ""
|
||||
in
|
||||
if got <> cond then
|
||||
fail "the %s trap is reported as %S, wanted %S" what got cond;
|
||||
(* What is on offer, which at a trap is a list nobody can take. It
|
||||
is still *listed*: "why can I not have that one" is a fair
|
||||
question, and an empty list would make a live restart-case look
|
||||
like it had been unwound. *)
|
||||
let r = ask "(:op \"break\")" in
|
||||
if status r <> "ok" then fail "break at the %s trap: %s" what (status r);
|
||||
(match Wire.field r "restarts" with
|
||||
| Some { Form.v = Form.List l; _ } ->
|
||||
let names =
|
||||
List.filter_map
|
||||
(fun (n : Form.t) ->
|
||||
match n.Form.v with Form.Str x -> Some x | _ -> None)
|
||||
l
|
||||
in
|
||||
if names <> restarts then
|
||||
fail "restarts at the %s trap: %s" what
|
||||
(String.concat ", " names)
|
||||
| _ -> fail "break at the %s trap listed no restarts field" what);
|
||||
(* And every one of them named as untakeable. This is the half the
|
||||
editor reads: the terminal listing says so in words, and a
|
||||
[:unreachable] that disagreed with it would be the two ends
|
||||
describing different programs — a restart shown as choosable,
|
||||
chosen, and then refused. *)
|
||||
(match Wire.field r "unreachable" with
|
||||
| Some { Form.v = Form.List l; _ } ->
|
||||
let idx =
|
||||
List.filter_map
|
||||
(fun (n : Form.t) ->
|
||||
match n.Form.v with Form.Int i -> Some (Int64.to_int i) | _ -> None)
|
||||
l
|
||||
in
|
||||
if idx <> List.mapi (fun i _ -> i) restarts then
|
||||
fail "unreachable restarts at the %s trap: %s" what
|
||||
(String.concat ", " (List.map string_of_int idx))
|
||||
| _ ->
|
||||
if restarts <> [] then
|
||||
fail "break at the %s trap named no unreachable restarts" what);
|
||||
(* And the refusal, where there is a name to refuse. Answered [err]
|
||||
with the reason rather than [ok] and then dropped, which is the
|
||||
shape that would tell an editor the program had resumed when it
|
||||
had not. *)
|
||||
List.iter
|
||||
(fun name ->
|
||||
let r =
|
||||
ask (Printf.sprintf "(:op \"restart\" :name %s)"
|
||||
(Wire.quote name))
|
||||
in
|
||||
if status r = "ok" then
|
||||
fail "the %s trap accepted a restart it cannot take" what)
|
||||
restarts;
|
||||
(* The session, still a session. This is the whole point: the daemon
|
||||
is answering, the compiler is in this process, and the program is
|
||||
standing still in front of it. *)
|
||||
let r =
|
||||
ask "(:op \"eval-expr\" :code \"(+ 2 2)\" :file \"/tmp/buf.flan\")"
|
||||
in
|
||||
if Wire.string_field r "value" <> Some "4" then
|
||||
fail "an expression while parked at the %s trap: %s" what
|
||||
(Option.value ~default:(status r) (Wire.string_field r "message"));
|
||||
(* Torn down by [abort], not by [close]: a trap parks for good, so
|
||||
there is no resume to wait for and nothing to gain by waiting. *)
|
||||
let r = ask "(:op \"abort\")" in
|
||||
if status r <> "ok" then
|
||||
fail "abort at the %s trap: %s" what
|
||||
(Option.value ~default:"" (Wire.string_field r "message"))
|
||||
end;
|
||||
Unix.close c;
|
||||
if not
|
||||
(await ~ms:5000 (fun () ->
|
||||
match Unix.waitpid [ Unix.WNOHANG ] tpid with
|
||||
| 0, _ -> false
|
||||
| _ -> true
|
||||
| exception Unix.Unix_error _ -> true))
|
||||
then begin
|
||||
fail "the daemon outlived the %s trap it aborted" what;
|
||||
(try Unix.kill tpid Sys.sigkill with Unix.Unix_error _ -> ());
|
||||
(try ignore (Unix.waitpid [] tpid) with Unix.Unix_error _ -> ())
|
||||
end
|
||||
end
|
||||
in
|
||||
trap_park "free-all" "dev-trap-free-all.flan" "NoFreeAll" [ "continue" ];
|
||||
trap_park "null allocator" "dev-trap-null-alloc.flan" "NullAllocator" [];
|
||||
|
||||
(* ── The locals of a stopped frame ─────────────────────────────── *)
|
||||
|
||||
(* A third daemon, over a program that stops with something worth looking
|
||||
|
||||
93
vendor/agent/flan_agent.c
vendored
93
vendor/agent/flan_agent.c
vendored
@ -255,6 +255,10 @@ static int publish(job j) {
|
||||
* runtime must not depend on an optional package for either. */
|
||||
extern void (*flan_break_hook)(const uint8_t *name, int64_t namelen,
|
||||
void *condition, void *xfer);
|
||||
/* The other hook, for the traps that call with no channel — see rt_trap in
|
||||
* flan_rt.c, which carries the argument for why they are two hooks and not
|
||||
* one. This end of it is [trap_stop] below. */
|
||||
extern void (*flan_trap_hook)(const uint8_t *name, int64_t namelen);
|
||||
extern int32_t flan_break_resume(const uint8_t *name, int64_t namelen,
|
||||
void *xfer);
|
||||
extern int32_t flan_restart_count(void);
|
||||
@ -354,6 +358,15 @@ static _Atomic int aborting;
|
||||
|
||||
typedef struct {
|
||||
int32_t gen; /* never reused, never 0 */
|
||||
/* Whether *any* restart on this list can be taken, which is a property of
|
||||
* the break and not of the restarts. [reachable] answers a different
|
||||
* question — that one is per restart, and it is about the thunk boundary.
|
||||
* A break taken by a trap with no transfer channel has a perfectly good
|
||||
* list of live restarts below it and no way to aim at one, so the whole
|
||||
* snapshot is marked instead of each entry: the refusal sentence differs,
|
||||
* and a reader of this struct should not have to infer which case a run of
|
||||
* zeroes in [reachable] meant. */
|
||||
int32_t resumable;
|
||||
int32_t n;
|
||||
int32_t total; /* before SNAP_MAX truncated it */
|
||||
void *frame[SNAP_MAX];
|
||||
@ -425,12 +438,13 @@ static snapshot *snap_top(void) {
|
||||
* to nest, which the caller reports rather than serving a stale one. */
|
||||
static int32_t snap_gen; /* monotone; 0 is "no snapshot" */
|
||||
|
||||
static int snap_push(void) {
|
||||
static int snap_push(int resumable) {
|
||||
int d = atomic_load(&snap_depth);
|
||||
if (d >= BREAK_MAX) return 0;
|
||||
snapshot *s = &snaps[d];
|
||||
int32_t n = flan_restart_count();
|
||||
s->gen = ++snap_gen;
|
||||
s->resumable = resumable;
|
||||
s->total = n;
|
||||
s->used = 0;
|
||||
s->n = 0;
|
||||
@ -536,8 +550,16 @@ static _Noreturn void die_now(void) {
|
||||
_exit(134);
|
||||
}
|
||||
|
||||
static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
|
||||
void *xfer) {
|
||||
/* The loop itself, with one bit of what the caller knows: whether a restart
|
||||
* taken here has anywhere to land. [break_loop] passes 1 — it holds the
|
||||
* signaller's channel, and writing a frame into it is how §2 resumes.
|
||||
* [trap_stop] passes 0, and then this is a place to *stand and read* and
|
||||
* nothing more: the stack, the locals, the globals and the restart list are
|
||||
* all still there to be looked at, and only the resume is refused. That is
|
||||
* strictly more than the alternative, which was the whole process exiting
|
||||
* before anyone could ask a question. */
|
||||
static void break_loop_at(const uint8_t *name, int64_t namelen, void *condition,
|
||||
void *xfer, int resumable) {
|
||||
struct timespec step = { 0, 2000000 }; /* 2ms */
|
||||
(void)condition;
|
||||
fflush(stdout);
|
||||
@ -548,7 +570,7 @@ static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
|
||||
* are then the same list, numbered the same way, and the numbers are what a
|
||||
* choice is made of. */
|
||||
int32_t my_gen;
|
||||
if (!snap_push()) {
|
||||
if (!snap_push(resumable)) {
|
||||
fflush(stdout);
|
||||
fprintf(stderr, "flan: %d nested break loops - giving up rather than "
|
||||
"spinning\n", BREAK_MAX);
|
||||
@ -558,7 +580,18 @@ static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
|
||||
{
|
||||
snapshot *s = snap_top();
|
||||
my_gen = s->gen;
|
||||
if (s->n == 0)
|
||||
/* Said once, above the list, rather than appended to every line: it is
|
||||
* one fact about the break, and repeating it per restart would read as
|
||||
* each restart having its own separate problem. The list is still
|
||||
* printed, because what is on offer is part of where the program is even
|
||||
* when none of it can be taken — and because the same names come back
|
||||
* from a `restarts' query, and the terminal and the socket must not be
|
||||
* describing two different programs. */
|
||||
if (!s->resumable)
|
||||
fprintf(stderr,
|
||||
" this trap has no transfer channel, so nothing here can be "
|
||||
"resumed into; read the frame, then fix and reload, or abort\n");
|
||||
else if (s->n == 0)
|
||||
fprintf(stderr, " no restarts are active; abort, or fix and reload\n");
|
||||
for (int32_t i = 0; i < s->n; i++)
|
||||
/* Numbered, because that is how one is taken now, and marked when it is
|
||||
@ -566,7 +599,9 @@ static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
|
||||
* 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)");
|
||||
!s->resumable ? " (cannot be taken from this trap)"
|
||||
: 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);
|
||||
}
|
||||
@ -614,7 +649,7 @@ static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
|
||||
atomic_store(&chosen_ready, 0);
|
||||
snapshot *s = snap_top();
|
||||
int ok = s != NULL && s->gen == my_gen && take >= 0 && take < s->n
|
||||
&& s->reachable[take];
|
||||
&& s->resumable && s->reachable[take];
|
||||
if (ok) {
|
||||
flan_restart_take(s->frame[take], xfer);
|
||||
fprintf(stderr, "flan: resuming at restart %d. %s\n", take,
|
||||
@ -644,6 +679,27 @@ static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
|
||||
}
|
||||
}
|
||||
|
||||
/* §2's break, which can resume, and the trap's, which cannot. Two names over
|
||||
* one loop because the two hooks have different signatures and different
|
||||
* promises, and a caller of either should not have to pass a flag it does not
|
||||
* understand.
|
||||
*
|
||||
* [trap_stop] is not marked _Noreturn even though it never returns: the loop
|
||||
* above leaves only by resuming, which needs [resumable], or by [die_now].
|
||||
* Saying so in the type would oblige this function to prove it, and the honest
|
||||
* proof is the [die_now] below — reached only if the loop is ever given a way
|
||||
* out that this call did not ask for, and dying there is what the program did
|
||||
* before any of this existed. */
|
||||
static void break_loop(const uint8_t *name, int64_t namelen, void *condition,
|
||||
void *xfer) {
|
||||
break_loop_at(name, namelen, condition, xfer, 1);
|
||||
}
|
||||
|
||||
static void trap_stop(const uint8_t *name, int64_t namelen) {
|
||||
break_loop_at(name, namelen, NULL, NULL, 0);
|
||||
die_now();
|
||||
}
|
||||
|
||||
/* Re-entrant, and it has to be: a thunk this runs may itself error, and the
|
||||
* break loop that catches it polls again from inside that very call. So a job
|
||||
* is *claimed* — tail advanced past it — before it is run, and both indices
|
||||
@ -860,8 +916,12 @@ static void handle_line(char *line, sink *o) {
|
||||
if (s == NULL) { reply(o, "err no restart snapshot\n"); return; }
|
||||
for (int32_t i = 0; i < s->n; i++) {
|
||||
char hdr[32];
|
||||
/* Both facts fold into the one flag, because the flag answers one
|
||||
* question — can this be taken — and a break taken by a trap can take
|
||||
* none of them. The terminal listing above says the same thing in
|
||||
* words; the two must not describe different programs. */
|
||||
int k = snprintf(hdr, sizeof hdr, "%d %c ", i,
|
||||
s->reachable[i] ? '+' : '-');
|
||||
(s->resumable && s->reachable[i]) ? '+' : '-');
|
||||
if (k > 0) emit(o, hdr, (size_t)k);
|
||||
emit(o, s->names + s->off[i], (size_t)s->len[i]);
|
||||
reply(o, "\n");
|
||||
@ -977,6 +1037,16 @@ static void handle_line(char *line, sink *o) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
/* Checked before [reachable], because it is the stronger fact and the one
|
||||
* with the better sentence: at a trap every restart is unreachable, and
|
||||
* answering with the thunk-boundary reason would send the reader looking
|
||||
* for an evaluation that is not there. */
|
||||
if (!s->resumable) {
|
||||
reply(o, "err this break was taken by a trap with no transfer channel, "
|
||||
"so no restart can be taken from it; read the frame, then fix "
|
||||
"and reload, or abort\n");
|
||||
return;
|
||||
}
|
||||
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
|
||||
@ -1017,6 +1087,12 @@ static void handle_line(char *line, sink *o) {
|
||||
reply(o, " is active\n");
|
||||
return;
|
||||
}
|
||||
if (!s->resumable) {
|
||||
reply(o, "err this break was taken by a trap with no transfer channel, "
|
||||
"so no restart can be taken from it; read the frame, then fix "
|
||||
"and reload, or abort\n");
|
||||
return;
|
||||
}
|
||||
if (!s->reachable[at]) {
|
||||
reply(o, "err restart ");
|
||||
reply(o, line + 8);
|
||||
@ -1491,5 +1567,6 @@ int32_t flan_agent_start(const uint8_t *path, int64_t len) {
|
||||
* socket and not before it: without a listener there is nobody to ask what
|
||||
* to do, and stopping forever is worse than the abort it replaces. */
|
||||
flan_break_hook = break_loop;
|
||||
flan_trap_hook = trap_stop;
|
||||
return 0;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user