An expression's module is unloaded; a redefinition's never can be
C-x C-e is the case that repeats - you evaluate expressions constantly and redefine functions occasionally - and it is also the one case where unloading is safe. The thunk is called directly by flan_reload_call rather than through a cell, and it takes no registry slot, so once it has returned nothing points into its text and the value it produced has been copied out. The module says so with flan_reload_transient and the agent dlcloses it. Skipping the registry matters for more than tidiness: the table holds 4096 names and an expression evaluated in a loop would have exhausted it. A module that publishes a body can never make this claim, since leaving a pointer behind is its whole purpose. Measured on a running program: sixteen expression evaluations retain zero mappings, each redefinition retains three, permanently and correctly.
This commit is contained in:
parent
8a94f16acd
commit
44e199186e
12
NEXT.md
12
NEXT.md
@ -751,6 +751,18 @@ An evaluation is **not** a declaration: the thunk is built against the program
|
|||||||
and never spliced into it, so `describe` does not fill up with `eval/N` for
|
and never spliced into it, so `describe` does not fill up with `eval/N` for
|
||||||
every expression ever typed.
|
every expression ever typed.
|
||||||
|
|
||||||
|
**The module is unloaded afterwards**, which is the one case where that is
|
||||||
|
safe. The thunk is called directly by `flan_reload_call` rather than through a
|
||||||
|
cell, and it takes no registry slot — so once it has returned, nothing points
|
||||||
|
into its text and the value it produced has been copied out. It declares that
|
||||||
|
with `@flan_reload_transient` and the agent `dlclose`s it. Measured: sixteen
|
||||||
|
expression evaluations retain **zero** mappings, where each *redefinition*
|
||||||
|
retains three, permanently and correctly — a module that publishes a body
|
||||||
|
exists precisely to leave a pointer behind, and can never claim this.
|
||||||
|
|
||||||
|
Skipping the registry matters for more than tidiness: the table holds 4096
|
||||||
|
names and an expression evaluated in a loop would exhaust it.
|
||||||
|
|
||||||
The test that matters is the same expression twice: the fixture increments
|
The test that matters is the same expression twice: the fixture increments
|
||||||
`ticks` every frame, so two evaluations must disagree. A value computed in the
|
`ticks` every frame, so two evaluations must disagree. A value computed in the
|
||||||
compiler, or read out of a copy of the program's state, would not.
|
compiler, or read out of a copy of the program's state, would not.
|
||||||
|
|||||||
24
lib/emit.ml
24
lib/emit.ml
@ -1005,8 +1005,17 @@ let redefinition ?(checks = true) ?(dev = false) ?(known = fun _ -> true)
|
|||||||
in
|
in
|
||||||
let targets = List.map target fns in
|
let targets = List.map target fns in
|
||||||
let m = new_module ~checks ~dev ~known p in
|
let m = new_module ~checks ~dev ~known p in
|
||||||
|
(* A thunk the module runs itself is excluded from all of this: it is called
|
||||||
|
directly by [flan_reload_call], so it needs no cell, must not be published
|
||||||
|
into one, and must not take a registry slot — there are 4096 of those and
|
||||||
|
an expression evaluated in a loop would exhaust them. Nothing pointing
|
||||||
|
into the module is also what lets the agent unload it afterwards. *)
|
||||||
|
let transient f = call = Some f in
|
||||||
let new_fns =
|
let new_fns =
|
||||||
List.filter (fun (f : Tast.fn) -> not (known f.Tast.name)) p.Tast.fns
|
List.filter
|
||||||
|
(fun (f : Tast.fn) ->
|
||||||
|
(not (known f.Tast.name)) && not (transient f.Tast.name))
|
||||||
|
p.Tast.fns
|
||||||
and new_globals =
|
and new_globals =
|
||||||
List.filter (fun (g : Tast.global) -> not (known g.Tast.gname)) p.Tast.globals
|
List.filter (fun (g : Tast.global) -> not (known g.Tast.gname)) p.Tast.globals
|
||||||
in
|
in
|
||||||
@ -1028,6 +1037,7 @@ let redefinition ?(checks = true) ?(dev = false) ?(known = fun _ -> true)
|
|||||||
the installer below. *)
|
the installer below. *)
|
||||||
List.iter
|
List.iter
|
||||||
(fun (f : Tast.fn) ->
|
(fun (f : Tast.fn) ->
|
||||||
|
if not (transient f.Tast.name) then
|
||||||
Buffer.add_string m.out
|
Buffer.add_string m.out
|
||||||
(if known f.Tast.name then
|
(if known f.Tast.name then
|
||||||
Printf.sprintf "%s = external global ptr\n" (cellname f.Tast.name)
|
Printf.sprintf "%s = external global ptr\n" (cellname f.Tast.name)
|
||||||
@ -1106,7 +1116,8 @@ let redefinition ?(checks = true) ?(dev = false) ?(known = fun _ -> true)
|
|||||||
p.Tast.globals;
|
p.Tast.globals;
|
||||||
List.iter
|
List.iter
|
||||||
(fun (f : Tast.fn) ->
|
(fun (f : Tast.fn) ->
|
||||||
if known f.Tast.name then
|
if transient f.Tast.name then ()
|
||||||
|
else if known f.Tast.name then
|
||||||
Buffer.add_string b
|
Buffer.add_string b
|
||||||
(Printf.sprintf " store ptr %s, ptr %s\n" (fname f.Tast.name)
|
(Printf.sprintf " store ptr %s, ptr %s\n" (fname f.Tast.name)
|
||||||
(cellname f.Tast.name))
|
(cellname f.Tast.name))
|
||||||
@ -1129,7 +1140,14 @@ let redefinition ?(checks = true) ?(dev = false) ?(known = fun _ -> true)
|
|||||||
Buffer.add_string m.out
|
Buffer.add_string m.out
|
||||||
(Printf.sprintf
|
(Printf.sprintf
|
||||||
"\ndefine void @flan_reload_call() {\nentry:\n call %s %s()\n \
|
"\ndefine void @flan_reload_call() {\nentry:\n call %s %s()\n \
|
||||||
ret void\n}\n" (ll Types.Unit) (fname fn))
|
ret void\n}\n" (ll Types.Unit) (fname fn));
|
||||||
|
(* Nothing outside this module refers to anything in it once the call has
|
||||||
|
returned — no cell holds an address in its text, the registry has no
|
||||||
|
slot for it, and the value it produced was copied out. So it says so,
|
||||||
|
and the agent unloads it. A module that publishes a body can never say
|
||||||
|
this: its whole purpose is to leave a pointer behind. *)
|
||||||
|
if fns = [ fn ] && consts = [] then
|
||||||
|
Buffer.add_string m.out "\n@flan_reload_transient = global i8 1\n"
|
||||||
| None -> ()
|
| None -> ()
|
||||||
end;
|
end;
|
||||||
finish m
|
finish m
|
||||||
|
|||||||
@ -175,6 +175,26 @@ let () =
|
|||||||
(String.concat " " c.Session.fns)
|
(String.concat " " c.Session.fns)
|
||||||
| exception Loc.Error (_, m) -> fail "redefining game-draw: %s" m);
|
| exception Loc.Error (_, m) -> fail "redefining game-draw: %s" m);
|
||||||
|
|
||||||
|
(* An expression's thunk leaves nothing behind, and the module says so, which
|
||||||
|
is what lets the agent unload it: nothing may point into its text
|
||||||
|
afterwards. So it is called directly rather than through a cell, and it
|
||||||
|
must not take a registry slot either — there are 4096 of those and an
|
||||||
|
expression evaluated in a loop would exhaust them. A module that publishes
|
||||||
|
a body can never say this; its whole purpose is to leave a pointer. *)
|
||||||
|
let t, _ = Session.create ~file:"programs/reload.flan" in
|
||||||
|
let e = Session.eval_expr t "(+ 1 2)" in
|
||||||
|
if not (has e.Session.ir "@flan_reload_transient") then
|
||||||
|
fail "an expression's module did not declare itself unloadable";
|
||||||
|
if not (has e.Session.ir "define void @flan_reload_call") then
|
||||||
|
fail "an expression's module carried no thunk to run";
|
||||||
|
if has e.Session.ir "flan.cellp.eval" then
|
||||||
|
fail "an expression's thunk took a registry slot";
|
||||||
|
if has e.Session.ir "call ptr @flan_dev_cell" then
|
||||||
|
fail "an expression's thunk was looked up by name";
|
||||||
|
let c = Session.eval t "(defn bump [] i64 (set counter (+ counter 1)) counter)" in
|
||||||
|
if has c.Session.ir "@flan_reload_transient" then
|
||||||
|
fail "a module that publishes a body claimed to be unloadable";
|
||||||
|
|
||||||
if !failures = 0 then print_endline "session: all tests passed"
|
if !failures = 0 then print_endline "session: all tests passed"
|
||||||
else begin
|
else begin
|
||||||
Printf.printf "\n%d failure(s)\n" !failures;
|
Printf.printf "\n%d failure(s)\n" !failures;
|
||||||
|
|||||||
12
vendor/agent/flan_agent.c
vendored
12
vendor/agent/flan_agent.c
vendored
@ -54,7 +54,11 @@ const char *flan_dev_result_get(uint64_t *gen, uint64_t *len);
|
|||||||
* the loader. Overflow drops the oldest request rather than stalling; a dev
|
* the loader. Overflow drops the oldest request rather than stalling; a dev
|
||||||
* loop that queues 64 reloads between two frames has a bigger problem. */
|
* loop that queues 64 reloads between two frames has a bigger problem. */
|
||||||
#define QUEUE 64
|
#define QUEUE 64
|
||||||
typedef struct { install_fn install; call_fn call; } job;
|
/* [handle] is set only for a module that declared itself transient — one that
|
||||||
|
* ran a thunk and left nothing behind. Everything else is kept mapped forever:
|
||||||
|
* a cell holds an address inside a module's text, and unloading it would leave
|
||||||
|
* every call site pointing at unmapped memory. */
|
||||||
|
typedef struct { install_fn install; call_fn call; void *handle; } job;
|
||||||
|
|
||||||
static job queue[QUEUE];
|
static job queue[QUEUE];
|
||||||
static atomic_uint head; /* written by the listener */
|
static atomic_uint head; /* written by the listener */
|
||||||
@ -83,6 +87,7 @@ int32_t flan_agent_poll(void) {
|
|||||||
if (j.install != NULL) { j.install(); n++; }
|
if (j.install != NULL) { j.install(); n++; }
|
||||||
/* After the install, so a thunk sees the bodies its own module published. */
|
/* After the install, so a thunk sees the bodies its own module published. */
|
||||||
if (j.call != NULL) { j.call(); }
|
if (j.call != NULL) { j.call(); }
|
||||||
|
if (j.handle != NULL) { dlclose(j.handle); }
|
||||||
}
|
}
|
||||||
atomic_store_explicit(&tail, t, memory_order_relaxed);
|
atomic_store_explicit(&tail, t, memory_order_relaxed);
|
||||||
return n;
|
return n;
|
||||||
@ -160,6 +165,9 @@ static void serve(int fd) {
|
|||||||
if (f == NULL) { reply(fd, "err no flan_reload_install\n"); return; }
|
if (f == NULL) { reply(fd, "err no flan_reload_install\n"); return; }
|
||||||
/* Optional: only an expression evaluation has one. */
|
/* Optional: only an expression evaluation has one. */
|
||||||
call_fn c = (call_fn)(uintptr_t)dlsym(h, "flan_reload_call");
|
call_fn c = (call_fn)(uintptr_t)dlsym(h, "flan_reload_call");
|
||||||
|
/* And only one that leaves nothing behind may be unloaded. */
|
||||||
|
void *transient =
|
||||||
|
(c == NULL) ? NULL : dlsym(h, "flan_reload_transient");
|
||||||
/* Answer before queueing, not after. The game thread can install and run
|
/* Answer before queueing, not after. The game thread can install and run
|
||||||
* to completion between the two, and a program that exits there would tear
|
* to completion between the two, and a program that exits there would tear
|
||||||
* down this connection with the reply still unwritten — which reaches the
|
* down this connection with the reply still unwritten — which reaches the
|
||||||
@ -167,7 +175,7 @@ static void serve(int fd) {
|
|||||||
reply(fd, "ok\n");
|
reply(fd, "ok\n");
|
||||||
/* "queued", not "installed": the store happens on the game thread, at a
|
/* "queued", not "installed": the store happens on the game thread, at a
|
||||||
* time this thread does not get to choose. */
|
* time this thread does not get to choose. */
|
||||||
publish((job){ f, c });
|
publish((job){ f, c, transient == NULL ? NULL : h });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user