The temp directory is left to the build-plumbing lane, and under Evil the break buffer takes only the keys it binds

The dead-session sweep and the remove-on-close half are dropped: the sweep contradicts the recorded non-goals, and the other lane owns removal on a clean close. The Evil keys are defined per state from the map's own bindings, so special-mode-map's h, SPC, < and - no longer shadow Evil's.
This commit is contained in:
Joseph Ferano 2026-09-25 07:40:33 +07:00
parent 5477c75909
commit 414e1e5ae6
5 changed files with 44 additions and 125 deletions

View File

@ -1640,14 +1640,13 @@ it was killed from outside. The test binaries now print a signal by name
** DONE test_dev daemons fail to bind under load ** DONE test_dev daemons fail to bind under load
CLOSED: [2026-09-25] CLOSED: [2026-09-25]
/tmp is a tmpfs, and every session left its =flan-dev-<pid>= build directory The failure was a full /tmp, not the load. /tmp is a tmpfs, and every session
behind (about 8MB; a test_dev run left 300MB). Several concurrent runs filled left its =flan-dev-<pid>= build directory behind (a test_dev run leaves about
it and the daemons after that died on the link with ENOSPC. A session now 300MB); a few concurrent runs filled it and each daemon after that died on its
removes its directory when it ends through =close=, and each new session link with ENOSPC. The fix is the session removing its own directory on a clean
removes the directories of pids that no longer exist. A live pid's directory is =close=, which the build-plumbing lane owns. The half-write test's abort also
never touched, so a failed build's IR stays until the next session starts. goes through =aborted= now, which took the same run down with an uncaught
The half-write test's abort also goes through =aborted= now, which took the =Wire.Closed=.
same run down with an uncaught =Wire.Closed=.
** DONE A test binary that hangs is killed by its own alarm ** DONE A test binary that hangs is killed by its own alarm
A reader branch that forgets to advance loops for ever and the suite waits as long A reader branch that forgets to advance loops for ever and the suite waits as long
@ -1799,8 +1798,9 @@ keyword may not be what resolved. The next report wants the exact form sent.
CLOSED: [2026-09-25] CLOSED: [2026-09-25]
Evil's normal state binds =0= (beginning of line), =1=-=9= (a count) and RET Evil's normal state binds =0= (beginning of line), =1=-=9= (a count) and RET
above the major mode's map, so the digit never reached =flan-cnr-take-number=. above the major mode's map, so the digit never reached =flan-cnr-take-number=.
=flan-cnr-mode-map= is now an Evil overriding map; keys it does not bind stay The keys =flan-cnr-mode-map= itself binds are given to Evil's normal and
Evil's. The other special-mode buffers (inspect, watch, doc, disassembly, motion states in that mode; every other key, including what =special-mode-map=
binds, stays Evil's. The other special-mode buffers (inspect, watch, doc, disassembly,
diagnostics, lower) have the same exposure and are not changed. diagnostics, lower) have the same exposure and are not changed.
** TODO Eval in the frame, from the break loop ** TODO Eval in the frame, from the break loop

View File

@ -704,11 +704,21 @@ anyone who would rather TAB always moved."
;; Evil's normal state binds 0, the other digits and RET above any major ;; Evil's normal state binds 0, the other digits and RET above any major
;; mode's map, so under Evil a digit moved point or started a count and took ;; mode's map, so under Evil a digit moved point or started a count and took
;; nothing. This map takes precedence over Evil's state maps instead; the keys ;; nothing. The keys this file binds are given to Evil's normal and motion
;; it does not bind, j and k among them, are still Evil's. ;; states in this mode. Only those keys: `flan-cnr-mode-map' inherits
;; `special-mode-map', and making it an overriding map would carry h, SPC, <
;; and - over from there as well. Every key not listed stays Evil's.
(with-eval-after-load 'evil (with-eval-after-load 'evil
(when (fboundp 'evil-make-overriding-map) (when (fboundp 'evil-define-key*)
(evil-make-overriding-map flan-cnr-mode-map))) ;; Collected first, and without the parent's bindings, because
;; `evil-define-key*' writes into the map being walked.
(let ((own nil))
(map-keymap-internal (lambda (key def)
(when (commandp def) (push (cons key def) own)))
flan-cnr-mode-map)
(dolist (b own)
(evil-define-key* '(normal motion) flan-cnr-mode-map
(vector (car b)) (cdr b))))))
(define-derived-mode flan-cnr-mode special-mode "flan-break" (define-derived-mode flan-cnr-mode special-mode "flan-break"
"What a stopped Flan program is offering." "What a stopped Flan program is offering."

View File

@ -1742,7 +1742,15 @@ stopped program, which is the case where it should fire."
(search-forward " 1: ") (search-forward " 1: ")
(execute-kbd-macro (kbd "RET")) (execute-kbd-macro (kbd "RET"))
(test-flan--check "under Evil, RET takes the restart on its line" (test-flan--check "under Evil, RET takes the restart on its line"
(equal sent '(:op "restart-at" :index 1 :name "skip")))) (equal sent '(:op "restart-at" :index 1 :name "skip")))
;; And the keys the buffer does not bind are still Evil's, not the
;; ones `special-mode-map' would bring with it.
(switch-to-buffer buf)
(dolist (k '(("h" . evil-backward-char) ("SPC" . evil-forward-char)
("<" . evil-shift-left)
("-" . evil-previous-line-first-non-blank)))
(test-flan--check (format "under Evil, %s is still Evil's" (car k))
(eq (key-binding (kbd (car k))) (cdr k)))))
(evil-mode -1)))) (evil-mode -1))))
(message "\n%d checks, %d failures" test-flan--ran test-flan--failures) (message "\n%d checks, %d failures" test-flan--ran test-flan--failures)

View File

@ -4241,72 +4241,6 @@ let accept_loop ?grace t ls =
in in
go () go ()
(* ── The session's directory ───────────────────────────────────────── *)
(* Every session builds into [$TMPDIR/flan-dev-<pid>]: the host executable,
its IR or listing, one module per evaluation. About 8MB before the first
evaluation, and nothing used to remove it, so every daemon that ever ran
left one behind. On a machine whose /tmp is a tmpfs that is memory, and a
test run starts some forty daemons: enough of them at once filled /tmp, and
the daemons after that died before binding, on the link, with "No space
left on device" — the "fail to bind under load" flake.
Two halves. A session that ends through [close] removes its own directory
on the way out. One that does not — a signal, a program that died where it
stood through [_exit], a failed build — cannot, so the next session to start
removes every [flan-dev-<pid>] whose process no longer exists. A pid that
is alive keeps its directory whoever it is now: a reused pid costs one
directory kept too long, never one removed from under a session.
Only ESRCH counts as gone. EPERM is a live process that belongs to someone
else, and a directory in a shared /tmp that is not ours fails to remove
anyway. *)
let dir_prefix = "flan-dev-"
let session_dir_of pid =
Filename.concat (Filename.get_temp_dir_name ())
(Printf.sprintf "%s%d" dir_prefix pid)
(* Best effort throughout: what cannot be removed stays. The chmod is for a
directory a session left read-only, which test_dev's cannot-write-a-module
case does on purpose. *)
let rec remove_tree path =
match Unix.lstat path with
| { Unix.st_kind = Unix.S_DIR; _ } ->
(try Unix.chmod path 0o700 with Unix.Unix_error _ -> ());
(match Sys.readdir path with
| names -> Array.iter (fun n -> remove_tree (Filename.concat path n)) names
| exception Sys_error _ -> ());
(try Unix.rmdir path with Unix.Unix_error _ -> ())
| _ -> (try Unix.unlink path with Unix.Unix_error _ -> ())
| exception Unix.Unix_error _ -> ()
let sweep_dead_sessions () =
let tmp = Filename.get_temp_dir_name () in
let own = Unix.getpid () and plen = String.length dir_prefix in
match Sys.readdir tmp with
| exception Sys_error _ -> ()
| names ->
Array.iter
(fun n ->
if String.length n > plen && String.sub n 0 plen = dir_prefix then
match int_of_string_opt (String.sub n plen (String.length n - plen)) with
| Some pid when pid > 0 && pid <> own ->
(match Unix.kill pid 0 with
| () -> ()
| exception Unix.Unix_error (Unix.ESRCH, _, _) ->
remove_tree (Filename.concat tmp n)
| exception Unix.Unix_error _ -> ())
| _ -> ())
names
(* This process's directory, after clearing out the dead ones. *)
let session_dir () =
sweep_dead_sessions ();
let dir = session_dir_of (Unix.getpid ()) in
(try Unix.mkdir dir 0o700 with Unix.Unix_error (Unix.EEXIST, _, _) -> ());
dir
(* [debug] is off by default, which keeps [flan dev] exactly what it was: a (* [debug] is off by default, which keeps [flan dev] exactly what it was: a
-O2 host and -O2 modules. It is opt-in rather than always-on because a debug -O2 host and -O2 modules. It is opt-in rather than always-on because a debug
build is an -O0 build — [llvm.dbg.declare] describes an alloca and mem2reg build is an -O0 build — [llvm.dbg.declare] describes an alloca and mem2reg
@ -4322,7 +4256,11 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () =
directory it was relative to. *) directory it was relative to. *)
let file = try Unix.realpath file with Unix.Unix_error _ -> file in let file = try Unix.realpath file with Unix.Unix_error _ -> file in
let session, l = Session.create ~debug ~x86 ~file () in let session, l = Session.create ~debug ~x86 ~file () in
let dir = session_dir () in let dir =
Filename.concat (Filename.get_temp_dir_name ())
(Printf.sprintf "flan-dev-%d" (Unix.getpid ()))
in
(try Unix.mkdir dir 0o700 with Unix.Unix_error (Unix.EEXIST, _, _) -> ());
let exe = Filename.concat dir "program" in let exe = Filename.concat dir "program" in
(* [keep] so the host's own IR survives the build. It is the text [llc] was (* [keep] so the host's own IR survives the build. It is the text [llc] was
actually given, not a second emission of it, which is the difference actually given, not a second emission of it, which is the difference
@ -4413,8 +4351,7 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () =
(try Unix.kill child Sys.sigterm with Unix.Unix_error _ -> ()); (try Unix.kill child Sys.sigterm with Unix.Unix_error _ -> ());
(try Unix.close ls with Unix.Unix_error _ -> ()); (try Unix.close ls with Unix.Unix_error _ -> ());
(try Unix.close rd with Unix.Unix_error _ -> ()); (try Unix.close rd with Unix.Unix_error _ -> ());
(try Unix.unlink sock with Unix.Unix_error _ -> ()); (try Unix.unlink sock with Unix.Unix_error _ -> ()))
remove_tree dir)
(fun () -> accept_loop t ls) (fun () -> accept_loop t ls)
(* ── One process: the program and the compiler in the same binary ──── *) (* ── One process: the program and the compiler in the same binary ──── *)
@ -5297,7 +5234,6 @@ let merged_serve () =
Printf.eprintf "flan dev: %s\n%!" (Printexc.to_string e)); Printf.eprintf "flan dev: %s\n%!" (Printexc.to_string e));
(try Unix.close ls with Unix.Unix_error _ -> ()); (try Unix.close ls with Unix.Unix_error _ -> ());
(try Unix.unlink sock with Unix.Unix_error _ -> ()); (try Unix.unlink sock with Unix.Unix_error _ -> ());
remove_tree t.dir;
(* [close] from the editor ends the session, and so now does an editor that (* [close] from the editor ends the session, and so now does an editor that
stopped being there; in one process either means the program too — which stopped being there; in one process either means the program too — which
is what the daemon did by killing its child. [_exit] for the loader-lock is what the daemon did by killing its child. [_exit] for the loader-lock
@ -5314,7 +5250,11 @@ let start_merged ?(debug = false) ?(x86 = true) ~file ~sock () =
let t0 = Unix.gettimeofday () in let t0 = Unix.gettimeofday () in
let file = try Unix.realpath file with Unix.Unix_error _ -> file in let file = try Unix.realpath file with Unix.Unix_error _ -> file in
let session, l = Session.create ~debug ~x86 ~file () in let session, l = Session.create ~debug ~x86 ~file () in
let dir = session_dir () in let dir =
Filename.concat (Filename.get_temp_dir_name ())
(Printf.sprintf "flan-dev-%d" (Unix.getpid ()))
in
(try Unix.mkdir dir 0o700 with Unix.Unix_error (Unix.EEXIST, _, _) -> ());
let exe = Filename.concat dir "program" in let exe = Filename.concat dir "program" in
(* The host's IR goes straight to its final home rather than being written (* The host's IR goes straight to its final home rather than being written
into the build's working directory and moved: the merged link is spelled into the build's working directory and moved: the merged link is spelled

View File

@ -233,33 +233,6 @@ let () =
rewrites: a true statement about the backend and no test of the verb. rewrites: a true statement about the backend and no test of the verb.
The default itself is checked further down, on a daemon that does not The default itself is checked further down, on a daemon that does not
need frames. *) need frames. *)
(* A session builds into [$TMPDIR/flan-dev-<pid>], about 8MB before its
first evaluation. Left behind by every daemon, those filled a tmpfs /tmp
under a few concurrent runs of this file, and every daemon after that
died on its link before binding. So: a dead session's directory is
swept by the next session to start, a live one's is not, and a session
that ends through [close] takes its own with it. The dead pid is a
process this test ran and reaped; this test's own pid is the live one. *)
let session_dir p =
Filename.concat (Filename.get_temp_dir_name ())
(Printf.sprintf "flan-dev-%d" p)
in
let dead =
let p =
Unix.create_process "true" [| "true" |] Unix.stdin Unix.stdout
Unix.stderr
in
ignore (Unix.waitpid [] p);
p
in
let plant p =
let d = session_dir p in
(try Unix.mkdir d 0o700 with Unix.Unix_error (Unix.EEXIST, _, _) -> ());
Out_channel.with_open_bin (Filename.concat d "program") (fun oc ->
output_string oc "left behind")
in
plant dead;
plant (Unix.getpid ());
let pid = let pid =
Unix.create_process flan Unix.create_process flan
[| flan; "dev"; "programs/dev-loop.flan"; "-s"; sock; "--llvm" |] [| flan; "dev"; "programs/dev-loop.flan"; "-s"; sock; "--llvm" |]
@ -270,15 +243,6 @@ let () =
if not (listening ~pid sock) then if not (listening ~pid sock) then
fail "the daemon %s" !listen_why fail "the daemon %s" !listen_why
else begin else begin
if Sys.file_exists (session_dir dead) then
fail "a dead session's directory %s outlived the next session's start"
(session_dir dead);
if not (Sys.file_exists (session_dir (Unix.getpid ()))) then
fail "a live process's session directory was swept";
(try Sys.remove (Filename.concat (session_dir (Unix.getpid ())) "program")
with Sys_error _ -> ());
(try Unix.rmdir (session_dir (Unix.getpid ()))
with Unix.Unix_error _ -> ());
(* The daemon owns the program's lifetime and kills it on [close], so (* The daemon owns the program's lifetime and kills it on [close], so
every step waits for the program to have got there. "ok" from an eval every step waits for the program to have got there. "ok" from an eval
means the module was queued, not that it has been installed. *) means the module was queued, not that it has been installed. *)
@ -850,9 +814,6 @@ let () =
transcript is the claim — after everything the first run printed and transcript is the claim — after everything the first run printed and
before anything the second did. *) before anything the second did. *)
ignore (Unix.waitpid [] pid); ignore (Unix.waitpid [] pid);
if Sys.file_exists (session_dir pid) then
fail "a session that ended through close left %s behind"
(session_dir pid);
let text = Buffer.contents output in let text = Buffer.contents output in
let wanted = "1\n5\n105\n777\npk\n106\n" in let wanted = "1\n5\n105\n777\npk\n106\n" in
if text <> wanted then if text <> wanted then