Merge: all six traps park for inspection, two with the resume refused

This commit is contained in:
Joseph Ferano 2026-09-18 13:48:16 +07:00
commit e6d85c80dd
6 changed files with 358 additions and 19 deletions

View File

@ -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.

View File

@ -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 ───────────────

View 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)

View 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)

View File

@ -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

View File

@ -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;
}