diff --git a/TODO.org b/TODO.org index 148b42ed..456f723c 100644 --- a/TODO.org +++ b/TODO.org @@ -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 CLOSED: [2026-09-25] -/tmp is a tmpfs, and every session left its =flan-dev-= build directory -behind (about 8MB; a test_dev run left 300MB). Several concurrent runs filled -it and the daemons after that died on the link with ENOSPC. A session now -removes its directory when it ends through =close=, and each new session -removes the directories of pids that no longer exist. A live pid's directory is -never touched, so a failed build's IR stays until the next session starts. -The half-write test's abort also goes through =aborted= now, which took the -same run down with an uncaught =Wire.Closed=. +The failure was a full /tmp, not the load. /tmp is a tmpfs, and every session +left its =flan-dev-= build directory behind (a test_dev run leaves about +300MB); a few concurrent runs filled it and each daemon after that died on its +link with ENOSPC. The fix is the session removing its own directory on a clean +=close=, which the build-plumbing lane owns. The half-write test's abort also +goes through =aborted= now, which took the same run down with an uncaught +=Wire.Closed=. ** 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 @@ -1799,8 +1798,9 @@ keyword may not be what resolved. The next report wants the exact form sent. CLOSED: [2026-09-25] 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=. -=flan-cnr-mode-map= is now an Evil overriding map; keys it does not bind stay -Evil's. The other special-mode buffers (inspect, watch, doc, disassembly, +The keys =flan-cnr-mode-map= itself binds are given to Evil's normal and +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. ** TODO Eval in the frame, from the break loop diff --git a/emacs/flan-cnr.el b/emacs/flan-cnr.el index 6b2ffb75..dea14019 100644 --- a/emacs/flan-cnr.el +++ b/emacs/flan-cnr.el @@ -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 ;; 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 -;; it does not bind, j and k among them, are still Evil's. +;; nothing. The keys this file binds are given to Evil's normal and motion +;; 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 - (when (fboundp 'evil-make-overriding-map) - (evil-make-overriding-map flan-cnr-mode-map))) + (when (fboundp 'evil-define-key*) + ;; 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" "What a stopped Flan program is offering." diff --git a/emacs/test-flan-cider.el b/emacs/test-flan-cider.el index aacc92d5..a13ead5d 100644 --- a/emacs/test-flan-cider.el +++ b/emacs/test-flan-cider.el @@ -1742,7 +1742,15 @@ stopped program, which is the case where it should fire." (search-forward " 1: ") (execute-kbd-macro (kbd "RET")) (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)))) (message "\n%d checks, %d failures" test-flan--ran test-flan--failures) diff --git a/lib/dev.ml b/lib/dev.ml index ca813039..693575a2 100644 --- a/lib/dev.ml +++ b/lib/dev.ml @@ -4241,72 +4241,6 @@ let accept_loop ?grace t ls = in go () -(* ── The session's directory ───────────────────────────────────────── *) - -(* Every session builds into [$TMPDIR/flan-dev-]: 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-] 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 -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 @@ -4322,7 +4256,11 @@ let two_process ?(debug = false) ?(x86 = true) ~file ~sock () = directory it was relative to. *) let file = try Unix.realpath file with Unix.Unix_error _ -> 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 (* [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 @@ -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.close ls with Unix.Unix_error _ -> ()); (try Unix.close rd with Unix.Unix_error _ -> ()); - (try Unix.unlink sock with Unix.Unix_error _ -> ()); - remove_tree dir) + (try Unix.unlink sock with Unix.Unix_error _ -> ())) (fun () -> accept_loop t ls) (* ── 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)); (try Unix.close ls 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 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 @@ -5314,7 +5250,11 @@ let start_merged ?(debug = false) ?(x86 = true) ~file ~sock () = let t0 = Unix.gettimeofday () in let file = try Unix.realpath file with Unix.Unix_error _ -> 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 (* 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 diff --git a/test/test_dev.ml b/test/test_dev.ml index 83387be3..fe2e9d6d 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -233,33 +233,6 @@ let () = 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 need frames. *) - (* A session builds into [$TMPDIR/flan-dev-], 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 = Unix.create_process flan [| flan; "dev"; "programs/dev-loop.flan"; "-s"; sock; "--llvm" |] @@ -270,15 +243,6 @@ let () = if not (listening ~pid sock) then fail "the daemon %s" !listen_why 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 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. *) @@ -850,9 +814,6 @@ let () = transcript is the claim — after everything the first run printed and before anything the second did. *) 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 wanted = "1\n5\n105\n777\npk\n106\n" in if text <> wanted then