A thunk that holds a string keeps its mapping

The transient marker said nothing outside the module points into it once the
call returns - true of its text, silent about its data. A string literal is
emitted into the evaluating module's own image and an expression may store one
anywhere: C-x C-e on (set msg "tuned") left a program global pointing into the
mapping the agent was about to drop. The next thunk can be mapped at the same
address, so what comes back is silent garbage rather than a fault, and nothing
in the compiler refused it.

The third condition is that the module emitted no string constants. Then there
is nothing in its image anyone could still be pointing at. One that did keeps
its mapping, which costs a page and is the bargain every redefinition already
makes.

Found by reading jank, which has met the neighbouring hazard from the other
side: its notes are explicit that nothing is ever unloaded, and the one place
Flan makes an exception is the one place the rule had a hole.
This commit is contained in:
Joseph Ferano 2026-09-11 20:50:23 +07:00
parent fe1237ccea
commit 5f0bde8149
2 changed files with 24 additions and 2 deletions

View File

@ -1480,8 +1480,18 @@ let redefinition ?(checks = true) ?(dev = false) ?(known = fun _ -> true)
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
this: its whole purpose is to leave a pointer behind.
[m.nstr = 0] is the third condition and it is about *data*, not text.
A string literal is emitted into this module's own image, and an
expression may store one anywhere it likes [(set msg "tuned")] on a
string global leaves that global pointing into the mapping the agent
is about to drop. The next thunk can be mapped at the same address, so
the result is silent garbage rather than a fault. A module with no
string constants has nothing in its image anyone could still be
pointing at; one with any keeps its mapping, which costs a page and is
the same bargain every redefinition already makes. *)
if fns = [ fn ] && consts = [] && m.nstr = 0 then
Buffer.add_string m.out "\n@flan_reload_transient = global i8 1\n"
| None -> ()
end;

View File

@ -208,6 +208,18 @@ let () =
if has c.Session.ir "@flan_reload_transient" then
fail "a module that publishes a body claimed to be unloadable";
(* And a third condition, about data rather than text. A string literal lives
in the evaluating module's own image, and an expression may store one
anywhere: [(set msg "x")] on a string global would leave that global
pointing into a mapping the agent then drops and since the next thunk can
be mapped at the same address, the result is silent garbage rather than a
fault. A module carrying any string constant keeps its mapping. *)
let str = Session.eval_expr t "(print-line \"tuned\")" in
if not (has str.Session.ir ".str.0") then
fail "the fixture stopped carrying a string constant, so it proves nothing";
if has str.Session.ir "@flan_reload_transient" then
fail "an expression holding a string claimed to be unloadable";
if !failures = 0 then print_endline "session: all tests passed"
else begin
Printf.printf "\n%d failure(s)\n" !failures;