From 9194f918fceef2295e5fdf9b62eb3efb906f4e74 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
There is no implicit widening. Every operand of an arithmetic or -comparison form has one type, and every conversion is written as a cast:
+A conversion that cannot change the number is implicit; any other is
+written. An i32 goes where an i64 or an f64
+is wanted, and an f32 where an f64 is. An i64 into an
+i32, or an i32 into an f32, is an error until it is
+written as a cast:
(defn main [] i32
(let [n 40 ; i32, inferred
- big (i64 n) ; every widening is written
+ big (i64 n) ; a cast is a call named for the type
x 1.5] ; f64
(print (+ big 2)) (println "")
(print (* x 2.5)) (println "")
@@ -1367,11 +1370,11 @@ quoted or it could not be told from the punctuation around it.
since the walk is unrolled at compile time, rather than putting ten thousand printing
sites in the module.
-That the walk takes the argument's own type matters more here than it would in a
-language that widens implicitly. Nothing widens implicitly in this one, so a printer
-that named a type would need a cast written at every call — and a u64
-above 263 put through a signed one comes out negative. print
-takes the value as it is and prints the number it holds.
+That the walk takes the argument's own type matters for the types that do not
+widen into one another. A printer that named one type, say i64, would take
+an i32 as it is but need a cast for a u64 — and a
+u64 above 263 put through that cast comes out negative.
+print takes the value as it is and prints the number it holds.
The prelude
From 991e4423aae0748f8c7c5dcc2af6770a66f065b6 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Fri, 25 Sep 2026 07:19:52 +0700
Subject: [PATCH 03/10] A dev session removes its build directory when it
closes, and the next session sweeps the ones whose process is gone
Every daemon left about 8MB in $TMPDIR/flan-dev-. A test_dev run left 300MB, and a few concurrent runs filled the tmpfs /tmp, after which every daemon died on its link before binding. The half-write test's abort also goes through [aborted] now, since an abort that worked can arrive as the socket closing.
---
lib/dev.ml | 82 +++++++++++++++++++++++++++++++++++++++++-------
test/test_dev.ml | 48 ++++++++++++++++++++++++++--
2 files changed, 116 insertions(+), 14 deletions(-)
diff --git a/lib/dev.ml b/lib/dev.ml
index 693575a2..ca813039 100644
--- a/lib/dev.ml
+++ b/lib/dev.ml
@@ -4241,6 +4241,72 @@ 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
@@ -4256,11 +4322,7 @@ 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 =
- 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 dir = session_dir () in
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
@@ -4351,7 +4413,8 @@ 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 _ -> ()))
+ (try Unix.unlink sock with Unix.Unix_error _ -> ());
+ remove_tree dir)
(fun () -> accept_loop t ls)
(* ── One process: the program and the compiler in the same binary ──── *)
@@ -5234,6 +5297,7 @@ 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
@@ -5250,11 +5314,7 @@ 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 =
- 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 dir = session_dir () in
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 5be18307..7ee6b241 100644
--- a/test/test_dev.ml
+++ b/test/test_dev.ml
@@ -216,6 +216,33 @@ 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" |]
@@ -226,6 +253,15 @@ 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. *)
@@ -797,6 +833,9 @@ 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
@@ -5605,9 +5644,12 @@ let () =
abort is refused by a program that is *running*, which is what a
failure above would leave behind — and this fixture then polls for
twenty seconds and parks, so the wait below would be a hang rather
- than a report. The signal is for that case only. *)
- if status (request c "(:op \"abort\")") <> "ok" then
- (try Unix.kill hpid Sys.sigkill with Unix.Unix_error _ -> ());
+ than a report. The signal is for that case only. Through [aborted],
+ because an abort that worked can arrive as the socket closing. *)
+ (match aborted c with
+ | Some r when status r <> "ok" ->
+ (try Unix.kill hpid Sys.sigkill with Unix.Unix_error _ -> ())
+ | _ -> ());
(try Unix.close c with Unix.Unix_error _ -> ());
(try ignore (Unix.waitpid [] hpid) with Unix.Unix_error _ -> ())
end;
From 2250548b0773e6e59d3cee04c7cda16031448cf7 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Fri, 25 Sep 2026 07:21:44 +0700
Subject: [PATCH 04/10] A test that sees a process die on a signal names the
signal, not OCaml's number for it
OCaml numbers the signals negatively and in its own order: SIGTERM is -11. The globals daemon's "transient signal 11" was a SIGTERM, not a segfault.
---
test/test_acceptance.ml | 8 ++++----
test/test_agent.ml | 12 ++++++------
test/test_dev.ml | 21 +++++++++++++++++++--
test/test_emacs.ml | 7 ++++---
test/test_repl.ml | 6 ++++--
test/test_support.ml | 22 ++++++++++++++++++++--
6 files changed, 57 insertions(+), 19 deletions(-)
diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml
index 58a8cf4e..283af120 100644
--- a/test/test_acceptance.ml
+++ b/test/test_acceptance.ml
@@ -186,8 +186,8 @@ module Pool = struct
name
(match status with
| Unix.WEXITED n -> Printf.sprintf "with code %d" n
- | Unix.WSIGNALED n -> Printf.sprintf "on signal %d" n
- | Unix.WSTOPPED n -> Printf.sprintf "stopped on signal %d" n))
+ | Unix.WSIGNALED n -> "on " ^ Test_support.signal_name n
+ | Unix.WSTOPPED n -> "stopped on " ^ Test_support.signal_name n))
| None, _ ->
Some
(Printf.sprintf
@@ -195,8 +195,8 @@ module Pool = struct
name
(match status with
| Unix.WEXITED n -> Printf.sprintf "exit %d" n
- | Unix.WSIGNALED n -> Printf.sprintf "signal %d" n
- | Unix.WSTOPPED n -> Printf.sprintf "stopped, signal %d" n))
+ | Unix.WSIGNALED n -> Test_support.signal_name n
+ | Unix.WSTOPPED n -> "stopped on " ^ Test_support.signal_name n))
in
ignore (Queue.pop inflight);
match msg with
diff --git a/test/test_agent.ml b/test/test_agent.ml
index 62a24bef..4d04b60a 100644
--- a/test/test_agent.ml
+++ b/test/test_agent.ml
@@ -149,8 +149,8 @@ let () =
fail "agent reload\n got: %S (%s)\n wanted: %S" text
(match status with
| Unix.WEXITED c -> Printf.sprintf "exit %d" c
- | Unix.WSIGNALED c -> Printf.sprintf "signal %d" c
- | Unix.WSTOPPED c -> Printf.sprintf "stopped %d" c)
+ | Unix.WSIGNALED c -> Test_support.signal_name c
+ | Unix.WSTOPPED c -> "stopped on " ^ Test_support.signal_name c)
"1\n1000\n1007\n"
end;
@@ -309,8 +309,8 @@ let () =
%S (exit 1)" text
(match !bstat with
| Unix.WEXITED c -> Printf.sprintf "exit %d" c
- | Unix.WSIGNALED c -> Printf.sprintf "signal %d" c
- | Unix.WSTOPPED c -> Printf.sprintf "stopped %d" c)
+ | Unix.WSIGNALED c -> Test_support.signal_name c
+ | Unix.WSTOPPED c -> "stopped on " ^ Test_support.signal_name c)
"cannot listen\n"
end;
@@ -766,8 +766,8 @@ let () =
fail "abort left status %s, wanted exit 134"
(match !lstatus with
| Unix.WEXITED c -> Printf.sprintf "exit %d" c
- | Unix.WSIGNALED c -> Printf.sprintf "signal %d" c
- | Unix.WSTOPPED c -> Printf.sprintf "stopped %d" c)
+ | Unix.WSIGNALED c -> Test_support.signal_name c
+ | Unix.WSTOPPED c -> "stopped on " ^ Test_support.signal_name c)
(* The other way out, and the one that skips atexit on purpose: [abort]
leaves by [_exit] so that it cannot hang on the loader lock, and the
socket is therefore unlinked by hand there. A file left here would
diff --git a/test/test_dev.ml b/test/test_dev.ml
index 7ee6b241..83387be3 100644
--- a/test/test_dev.ml
+++ b/test/test_dev.ml
@@ -196,6 +196,23 @@ let () =
if not (contains_sub other "flan.abi.x86") then
fail "a parked program's other refusals were rewritten too: %S" other
+(* A daemon's death is reported by the signal's name. [WSIGNALED] carries
+ OCaml's own numbering, in which SIGTERM is -11, and a SIGTERM printed as
+ "signal -11" was once recorded as a segfault. A real process, really
+ terminated, so the number is whatever [waitpid] hands back. *)
+let () =
+ let p =
+ Unix.create_process "sleep" [| "sleep"; "30" |] Unix.stdin Unix.stdout
+ Unix.stderr
+ in
+ Unix.kill p Sys.sigterm;
+ match Unix.waitpid [] p with
+ | _, Unix.WSIGNALED n when Test_support.signal_name n = "SIGTERM" -> ()
+ | _, Unix.WSIGNALED n ->
+ fail "a process ended by SIGTERM is reported as %s"
+ (Test_support.signal_name n)
+ | _, _ -> fail "a process sent SIGTERM did not end on a signal"
+
let () =
match Sys.command "command -v clang > /dev/null 2>&1 && command -v llc > /dev/null 2>&1" with
| 0 ->
@@ -5143,8 +5160,8 @@ let () =
(match st with
| Unix.WEXITED n -> Printf.sprintf "exit %d" n
| Unix.WSIGNALED n when n = Sys.sigpipe -> "killed by SIGPIPE"
- | Unix.WSIGNALED n -> Printf.sprintf "signal %d" n
- | Unix.WSTOPPED n -> Printf.sprintf "stopped on %d" n);
+ | Unix.WSIGNALED n -> Test_support.signal_name n
+ | Unix.WSTOPPED n -> "stopped on " ^ Test_support.signal_name n);
None
| exception Unix.Unix_error _ -> Some (connect rsock)
in
diff --git a/test/test_emacs.ml b/test/test_emacs.ml
index ecee324d..c2b2ebe1 100644
--- a/test/test_emacs.ml
+++ b/test/test_emacs.ml
@@ -112,9 +112,10 @@ let () =
match !status with
| Some (Unix.WEXITED n) -> Printf.sprintf "exited with status %d" n
| Some (Unix.WSIGNALED n) ->
- Printf.sprintf "was killed by signal %d — nothing ran its cleanup, so \
- the socket file is stale rather than gone" n
- | Some (Unix.WSTOPPED n) -> Printf.sprintf "stopped on signal %d" n
+ Printf.sprintf "was killed by %s — nothing ran its cleanup, so \
+ the socket file is stale rather than gone"
+ (Test_support.signal_name n)
+ | Some (Unix.WSTOPPED n) -> Printf.sprintf "stopped on %s" (Test_support.signal_name n)
| None -> "was still running when the client finished, and was terminated"
in
(* Read before the removal below, because on a failure this is the evidence
diff --git a/test/test_repl.ml b/test/test_repl.ml
index 9579130f..752b0679 100644
--- a/test/test_repl.ml
+++ b/test/test_repl.ml
@@ -326,9 +326,11 @@ let () =
"the daemon had already been killed by SIGPIPE — a reply written \
into a socket whose reader had gone"
| Some (Unix.WSIGNALED n) ->
- Printf.printf "the daemon had already been killed by signal %d\n" n
+ Printf.printf "the daemon had already been killed by %s\n"
+ (Test_support.signal_name n)
| Some (Unix.WSTOPPED n) ->
- Printf.printf "the daemon was stopped on signal %d\n" n
+ Printf.printf "the daemon was stopped on %s\n"
+ (Test_support.signal_name n)
| None -> print_endline "the daemon was still running at the end");
Printf.printf "\n-- the program's own output, last 4k --\n%s\n" prog_out;
exit 1
diff --git a/test/test_support.ml b/test/test_support.ml
index 41228ca6..a64b73a7 100644
--- a/test/test_support.ml
+++ b/test/test_support.ml
@@ -152,6 +152,24 @@ let rec connect ?(ms = 5000) path =
thing after a failed wait is always the report of it. *)
let listen_why = ref ""
+(* A signal as [WSIGNALED] carries it, by name. OCaml numbers the signals it
+ knows negatively and in its own order — [Sys.sigterm] is -11 and
+ [Sys.sigsegv] is -10 — so printing the number reads as the wrong signal to
+ anyone who knows the POSIX table: a daemon terminated by SIGTERM was once
+ recorded here as "a transient signal 11", a segfault that never happened. *)
+let signal_name n =
+ let known =
+ [ Sys.sigabrt, "SIGABRT"; Sys.sigalrm, "SIGALRM"; Sys.sigfpe, "SIGFPE";
+ Sys.sighup, "SIGHUP"; Sys.sigill, "SIGILL"; Sys.sigint, "SIGINT";
+ Sys.sigkill, "SIGKILL"; Sys.sigpipe, "SIGPIPE"; Sys.sigquit, "SIGQUIT";
+ Sys.sigsegv, "SIGSEGV"; Sys.sigterm, "SIGTERM"; Sys.sigusr1, "SIGUSR1";
+ Sys.sigusr2, "SIGUSR2"; Sys.sigchld, "SIGCHLD"; Sys.sigbus, "SIGBUS";
+ Sys.sigtrap, "SIGTRAP"; Sys.sigxcpu, "SIGXCPU" ]
+ in
+ match List.assoc_opt n known with
+ | Some s -> s
+ | None -> Printf.sprintf "signal %d" n
+
let listening ?(ms = 30000) ~pid path =
let died = ref None in
ignore
@@ -171,10 +189,10 @@ let listening ?(ms = 30000) ~pid path =
| Some (Unix.WEXITED n) ->
Printf.sprintf "exited with status %d before binding %s" n path
| Some (Unix.WSIGNALED n) ->
- Printf.sprintf "was killed by signal %d before binding %s" n path
+ Printf.sprintf "was killed by %s before binding %s" (signal_name n) path
(* Unreachable without WUNTRACED, and here only for exhaustiveness. *)
| Some (Unix.WSTOPPED n) ->
- Printf.sprintf "stopped on signal %d without binding %s" n path
+ Printf.sprintf "stopped on %s without binding %s" (signal_name n) path
| None ->
Printf.sprintf
"was still running after %ds without binding %s, so it was the \
From 04f5d5a3493465ea9876420a2515ece3fe7b6256 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Fri, 25 Sep 2026 07:22:42 +0700
Subject: [PATCH 05/10] Under Evil, a digit in the break buffer takes the
restart, because the buffer's keys take precedence over Evil's
---
emacs/flan-cnr.el | 8 ++++++++
emacs/test-flan-cider.el | 41 ++++++++++++++++++++++++++++++++++++++++
2 files changed, 49 insertions(+)
diff --git a/emacs/flan-cnr.el b/emacs/flan-cnr.el
index 72b3dbcd..6b2ffb75 100644
--- a/emacs/flan-cnr.el
+++ b/emacs/flan-cnr.el
@@ -702,6 +702,14 @@ anyone who would rather TAB always moved."
map)
"Keys in `flan-cnr-mode'.")
+;; 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.
+(with-eval-after-load 'evil
+ (when (fboundp 'evil-make-overriding-map)
+ (evil-make-overriding-map flan-cnr-mode-map)))
+
(define-derived-mode flan-cnr-mode special-mode "flan-break"
"What a stopped Flan program is offering."
(setq buffer-read-only t))
diff --git a/emacs/test-flan-cider.el b/emacs/test-flan-cider.el
index a0247452..aacc92d5 100644
--- a/emacs/test-flan-cider.el
+++ b/emacs/test-flan-cider.el
@@ -1704,6 +1704,47 @@ stopped program, which is the case where it should fire."
(file-name-directory load-file-name))
nil t)
+;; The break buffer's keys under Evil, pressed through the command loop rather
+;; than called. Evil's normal state binds 0 (beginning of line), 1-9 (a count)
+;; and RET (next line) above any major mode's map, so without the buffer's map
+;; taking precedence a digit moved point and took nothing. Run only where
+;; Evil is installed: -Q loads no packages, so it is looked for in the two
+;; places a package install puts it, and turned off again afterwards so nothing
+;; above or below this runs under it.
+(let* ((dirs (append (file-expand-wildcards "~/.config/emacs/elpa/evil-[0-9]*")
+ (file-expand-wildcards "~/.emacs.d/elpa/evil-[0-9]*")
+ (file-expand-wildcards "~/.config/emacs/elpa/goto-chg-*")
+ (file-expand-wildcards "~/.emacs.d/elpa/goto-chg-*")))
+ (load-path (append dirs load-path)))
+ (if (not (require 'evil nil t))
+ (message " skip the break buffer under Evil (Evil is not installed)")
+ (evil-mode 1)
+ (unwind-protect
+ (let* ((sent nil)
+ (flan-cnr-request-function
+ (lambda (form) (setq sent form) (list :status "ok")))
+ (buf (test-flan--cnr
+ (list :condition "Missing" :restarts '("retry" "skip")))))
+ (switch-to-buffer buf)
+ (evil-initialize-state)
+ (goto-char (point-min))
+ (execute-kbd-macro (kbd "0"))
+ (test-flan--check "under Evil, 0 takes restart 0"
+ (equal sent '(:op "restart-at" :index 0 :name "retry")))
+ (setq sent nil)
+ (switch-to-buffer buf)
+ (execute-kbd-macro (kbd "1"))
+ (test-flan--check "under Evil, 1 takes restart 1"
+ (equal sent '(:op "restart-at" :index 1 :name "skip")))
+ (setq sent nil)
+ (switch-to-buffer buf)
+ (goto-char (point-min))
+ (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"))))
+ (evil-mode -1))))
+
(message "\n%d checks, %d failures" test-flan--ran test-flan--failures)
(kill-emacs (if (> test-flan--failures 0) 1 0))
From 5477c759090739a3ea8ae4611031be9624adbbee Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Fri, 25 Sep 2026 07:24:20 +0700
Subject: [PATCH 06/10] Three of the four under-load and dogfooding entries are
settled, and the fourth records what was tried
---
TODO.org | 44 +++++++++++++++++++++++++++++---------------
1 file changed, 29 insertions(+), 15 deletions(-)
diff --git a/TODO.org b/TODO.org
index fcf9257b..148b42ed 100644
--- a/TODO.org
+++ b/TODO.org
@@ -1631,16 +1631,23 @@ sanitizer flag to it. Named as the check worth adding next; a day rather than an
hour. The x86 backend is not a gap here — that pair is refused by name, because
there is no sanitizer pass over hand-written assembly.
-** TODO A transient signal 11 on a globals daemon
-Seen once, never reproduced, on a daemon whose fixture had just gained a host
-global =Vec= and a run-time-new one. A reproduction under load would settle it.
+** DONE A transient signal 11 on a globals daemon
+CLOSED: [2026-09-25]
+Not a segfault. The report was OCaml's signal number, and in OCaml's numbering
+-11 is SIGTERM (SIGSEGV is -10). Nothing in the daemon sends itself SIGTERM, so
+it was killed from outside. The test binaries now print a signal by name
+(=Test_support.signal_name=); a number from =WSIGNALED= is never printed raw.
-** TODO test_dev daemons fail to bind under load
-Daemons exiting with status 1 or 2 before binding their socket, across most of the
-file at once, while the load average is high. No stale socket or leftover daemon
-afterwards; green on a quiet machine. Distinct from the registry race and the
-stale-park re-run flake, both of which are fixed. A daemon that dies before it
-binds died on the compiler's side, before any program it built has run a line.
+** 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=.
** 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
@@ -1781,13 +1788,20 @@ function of that name. Not reproduced: the file type checks, =flan reload= of
the same form builds, and a minimal =defclass= + =get= + =return= program
compiles. So it is the session path against an installed program, and what is
missing is what that daemon had installed at the time.
+Also not reproduced against a live daemon (2026-09-25): an =eval= of the
+=defclass=, of a =defn= doing the =get= and =return=, and of both in one form,
+and an =eval-expr= of the =get=, all succeed. In a session every installed
+function of no arguments returning =()= has the type =(CFn [] ())=, not only
+=pause= — a bare =pause= or =tick= asked of the session says so — so the
+keyword may not be what resolved. The next report wants the exact form sent.
-** TODO A digit does not take the restart RET takes
-Pressing =0= left the program stopped; RET on the same line resumed it. Both
-end in =flan-cnr-take=, but the digit path (=flan-cnr-take-number=,
-=flan-cnr.el:559=) scans from =point-min= for the line whose =flan-cnr-index=
-matches and calls =take= inside a =save-excursion=. Not reproduced yet — needs
-a non-raylib program stopped under a test daemon.
+** DONE A digit does not take the restart RET takes
+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,
+diagnostics, lower) have the same exposure and are not changed.
** TODO Eval in the frame, from the break loop
An expression is evaluated at a frame boundary, so it sees globals and not the
From cbd910c117cf6e5020f59ae349b42c23ccd9b114 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Fri, 25 Sep 2026 07:36:52 +0700
Subject: [PATCH 07/10] An arena's budget holds when a block grows in place,
and a shrink is never over budget
---
runtime/flan_rt.c | 5 ++++-
test/programs/exhausted.flan | 18 ++++++++++++++++++
test/test_acceptance.ml | 2 +-
3 files changed, 23 insertions(+), 2 deletions(-)
diff --git a/runtime/flan_rt.c b/runtime/flan_rt.c
index 00a9ddef..f295fa44 100644
--- a/runtime/flan_rt.c
+++ b/runtime/flan_rt.c
@@ -1185,7 +1185,7 @@ static void *flan_heap_proc(flan_allocator *a, int32_t mode, void *p,
* caller passes old_size for exactly this reason, and it is the one
* number a wrong answer here would read off the end of. */
void *q;
- if (flan_over_budget(a, size - old_size)) return NULL;
+ if (size > old_size && flan_over_budget(a, size - old_size)) return NULL;
q = flan_heap_proc(a, FLAN_ALLOC_ALLOC, NULL, 0, size, align);
if (!q) return NULL;
if (p && old_size > 0)
@@ -1331,6 +1331,9 @@ static void *flan_arena_proc(flan_allocator *a, int32_t mode, void *p,
* without copying, which is the common shape. */
if (p && (uint8_t *)p + old_size == ar->base + ar->offset) {
int64_t end = (int64_t)((uint8_t *)p - ar->base) + size;
+ /* The budget is checked here as on every other path; a block grown in
+ * place is still more live bytes. */
+ if (size > old_size && flan_over_budget(a, size - old_size)) return NULL;
if (end > ar->cap || end < 0) return NULL;
ar->offset = end;
if (end > ar->peak) ar->peak = end;
diff --git a/test/programs/exhausted.flan b/test/programs/exhausted.flan
index 206c99bc..0a1a1e2d 100644
--- a/test/programs/exhausted.flan
+++ b/test/programs/exhausted.flan
@@ -115,6 +115,24 @@
(println "INSERTIONSORT"))) ; INSERTIONSORT
(println failures) ; 1 — failed once, retried once
+ ;; And an arena, whose budget is checked when a Vec grows its block in
+ ;; place as well as when it allocates a new one. A Vec that is the only thing
+ ;; pushing into an arena always grows in place, so without that check the
+ ;; ceiling would never be met.
+ (set tight (arena-new 65536))
+ (set-alloc-budget tight 64)
+ (set failures 0)
+ (handler-bind
+ [(StorageExhausted [c]
+ (set failures (+ failures 1))
+ (set-alloc-budget tight (* 2 (alloc-budget tight)))
+ (invoke-restart 'retry))]
+ (let [v (vec-new i32 tight)]
+ (dotimes [i 1000] (push v i))
+ (println (length v)) ; 1000
+ (println (at v 999)))) ; 999
+ (println (> failures 0)) ; true
+
;; And the restart is not once-per-program: it is established at each
;; allocation, so a later one offers it again.
(set-alloc-budget tight 0)
diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml
index 58a8cf4e..d8f8d00d 100644
--- a/test/test_acceptance.ml
+++ b/test/test_acceptance.ml
@@ -1663,7 +1663,7 @@ let () =
flow an optimiser would otherwise launder. *)
let exhausted_out =
"64\n0\n126\ntrue\ntrue\n4\ntrue\n0\n8\n7\ntrue\n\
- 13\n73\n84\n90\nINSERTIONSORT\n1\n"
+ 13\n73\n84\n90\nINSERTIONSORT\n1\n1000\n999\ntrue\n"
in
outputs "storage exhausted, retried" "programs/exhausted.flan" exhausted_out;
outputs ~opt:"-O0" "storage exhausted, retried, -O0" "programs/exhausted.flan"
From cf71caefc237f622cfd4a38761347ed256c984b0 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Fri, 25 Sep 2026 07:36:57 +0700
Subject: [PATCH 08/10] spec-memory.md cites the defer_ok field on the line it
is declared
---
spec-memory.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/spec-memory.md b/spec-memory.md
index 578dcc11..1220f8f3 100644
--- a/spec-memory.md
+++ b/spec-memory.md
@@ -501,7 +501,7 @@ rejected:
- **Odin's `defer delete`** can be written only where its scope is the whole
function. `defer` is function-scoped: it is accepted at the top level of a
function body and in a `let` that is itself at the top level, to any depth,
- and refused in a branch or a loop body (`check.ml:477`, the `defer_ok` field,
+ and refused in a branch or a loop body (`check.ml:495`, the `defer_ok` field,
and the `Ast.Defer` arm at `check.ml:3661`). So `(defer (free v))` for a
`let`-bound `v` works when that `let` is at the top level of the body, and
runs at function exit rather than at the end of the `let`. There is no
From 918edfb0b56783cccdd6cd3bf2ce9883453e9562 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Fri, 25 Sep 2026 07:37:37 +0700
Subject: [PATCH 09/10] plan.org describes containers, pools and classes as
they are after the repeal
---
TODO.org | 10 +++++++---
plan.org | 38 ++++++++++++++++++++------------------
2 files changed, 27 insertions(+), 21 deletions(-)
diff --git a/TODO.org b/TODO.org
index 25ab769a..95a4021a 100644
--- a/TODO.org
+++ b/TODO.org
@@ -1966,9 +1966,13 @@ CLOSED: [2026-09-25]
The five predicates are the checker's five, with =integer?= in place of
=copyable?=, and the section no longer names =(Handle $t)= or =pool-new=.
-** TODO plan.org's Data model section still describes move-only containers
-"Owning containers move rather than copy on assignment" and "one =Vec= field
-makes it move-only" predate the repeal, under which everything copies.
+** DONE plan.org's Data model section still describes move-only containers
+CLOSED: [2026-09-25]
+The Data model section says assignment copies a container's header and the
+copies alias one buffer. The memory tiers and the classes section name the pool
+and generational handles as a library over a =Vec=, and the classes gate that
+named =Handle= is replaced by what classes are as built. The milestone record
+of what was frozen is left as history.
** DONE plan.org cites the wrong mechanism for jank's relinking bug
CLOSED: [2026-09-25]
diff --git a/plan.org b/plan.org
index 1498b8da..e8b45efd 100644
--- a/plan.org
+++ b/plan.org
@@ -5,7 +5,7 @@
Two documents are normative and are settled ahead of implementation. Anything in
this plan that contradicts them is out of date.
- [[file:spec-memory.md][spec-memory.md]] — ownership, the four container types,
- copies and moves, assignable places, generics without type classes, function
+ copies, assignable places, generics without type classes, function
values.
- [[file:spec-conditions.md][spec-conditions.md]] — the six hard cases of
conditions/restarts: what ~signal~ returns, no-handler behaviour, restart
@@ -56,14 +56,15 @@ Allocators all the way down; ~malloc~ hidden behind them.
| Tier | Strategy | Cost |
|-----------+---------------------------------+----------------|
| Frame | arena, bulk reset each frame | free |
-| Entities | pool + generational handles | free |
+| Entities | a pool over a ~Vec~, in a library | free |
| Subsystem | region, freed wholesale | free |
| Dev/REPL | leaks by design, reset on reload| dev only |
- Allocator is part of the calling convention, so a refcounted allocator can be
added later without a language change.
-- Generational handles instead of pointers for cross-references: a stale
- reference is detectable, not undefined behaviour.
+- Generational handles instead of pointers for cross-references, written as a
+ library over a ~Vec~ rather than provided by the language: a stale reference
+ is detectable, not undefined behaviour.
- Symbols and code live in a permanent arena that only grows.
** Why no persistent collections
@@ -93,8 +94,8 @@ world.
|-------------+-----------------+------------------+------|
| ~[n T]~ | inline, n items | copies | no |
| ~[T]~ | ptr+len | copies the view | no |
- | ~(Vec T)~ | ptr+len+cap | *moves* | yes |
- | ~(Map K V)~ | open addressing | *moves* | yes |
+ | ~(Vec T)~ | ptr+len+cap | copies the header | yes |
+ | ~(Map K V)~ | open addressing | copies the header | yes |
~Vec~ and ~Map~ are monomorphic on element type and record their allocator. Not
a Lua-style array/hash hybrid — that is what makes Lua's layout and performance
unpredictable.
@@ -105,17 +106,15 @@ world.
returns ~(Option V)~; ~put~ is the ~()~-returning upsert. See
spec-memory.md for the deferred move-aware operations.
- Operations: ~get~, ~put~, ~remove~, ~push~, ~pop~, ~at~, ~length~, ~update~.
- Copying is explicit: ~(clone m)~, and owning containers move rather than copy on
- assignment. No ~!~ convention — nothing is immutable, so it
+ An independent copy is explicit: ~(clone m)~. Assignment copies a container's
+ header, and the two headers alias one buffer. No ~!~ convention — nothing is immutable, so it
would carry no information. No ~assoc~; it only existed as the copy-returning form.
- ~const~ qualifier on references and slices: compile-time contract that a
callee will not mutate. Zero runtime cost.
-- Value structs copy on assignment — but only *value* structs. Ownership is
- structural: a struct is a value type iff every field is, so one ~Vec~ field
- makes it move-only. This is what keeps "copies on assignment" from meaning a
- shallow copy that aliases owned storage. Deep copies are always explicit:
- ~(clone x)~. Value structs are the snapshot / undo / replay story; they need no
- separate type.
+- Structs copy on assignment. A struct with a ~Vec~ field copies the header, so
+ the two copies alias one buffer, as in Odin. Deep copies are always explicit:
+ ~(clone x)~. Structs without owning fields are the snapshot / undo / replay
+ story; they need no separate type.
- Literals live in read-only memory.
- *A global's initialiser may be computed.* A value the linker can write goes
into the image and costs nothing to start; anything else is stored at
@@ -156,8 +155,8 @@ identity, extensibility, and live schema changes:
Classes require a managed allocation strategy and runtime class/shape metadata,
but *not necessarily a tracing GC*. The initial likely choices are a world or
-session arena, pool allocation behind generational ~(Handle T)~ values, or an
-explicitly owned region. A small tracing GC confined to class instances remains
+session arena, a library pool behind generational handles, or an explicitly
+owned region. A small tracing GC confined to class instances remains
an option if cyclic graphs prove burdensome; it never changes ~struct~ layout or
the C ABI.
@@ -187,8 +186,11 @@ surprising lazy mutation on field access:
#+end_src
The precise class syntax, inheritance, storage strategy, and migration API are
-not frozen. Do not add classes until ordinary ~struct~, ~Handle~, and reload
-semantics are working.
+not frozen. What is built is narrower than this section: a ~defclass~ instance
+is a dyn map with its class in the object header, and a redefined class
+migrates each instance lazily at its next touch, so nothing enumerates live
+instances (TODO.org, "defclass is a named dyn map with a shape tag" and "A
+redefined defclass migrates its instances lazily").
Immutability also serves the optimiser: a value known never to be mutated can be
copied into registers and stack-allocated freely. Mutability is what forces heap
From 414e1e5ae6e999061e1d9fcb7cd4b6fb63f46e31 Mon Sep 17 00:00:00 2001
From: Joseph Ferano
Date: Fri, 25 Sep 2026 07:40:33 +0700
Subject: [PATCH 10/10] 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.
---
TODO.org | 20 +++++-----
emacs/flan-cnr.el | 18 +++++++--
emacs/test-flan-cider.el | 10 ++++-
lib/dev.ml | 82 ++++++----------------------------------
test/test_dev.ml | 39 -------------------
5 files changed, 44 insertions(+), 125 deletions(-)
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