The leak sweeps have switches, a C source that includes the dyn header is recompiled when the header changes, and the map-remove and map-keys programs free what they allocate

This commit is contained in:
Joseph Ferano 2026-09-25 15:38:02 +07:00
parent 6e29c19097
commit a615e4ecfc
7 changed files with 45 additions and 18 deletions

View File

@ -1431,12 +1431,14 @@ through a pointer into it is answerable; memcheck is told the same fact, so the
same read is reported. The two stay two claims — different tools reaching same read is reported. The two stay two claims — different tools reaching
different people. different people.
** NEXT The leak question across the corpus ** WAIT The leak question across the corpus
Decided 2026-09-25: one pass over the whole corpus with LeakSanitizer and memcheck's leak check on. Memory an allocator holds by design is set aside; memory nothing owns is a leak and is fixed. The sweeps' default stays leak-checking off. Decided 2026-09-25: one pass over the whole corpus with LeakSanitizer and memcheck's leak check on. Memory an allocator holds by design is set aside; memory nothing owns is a leak and is fixed. The sweeps' default stays leak-checking off.
Both sweeps run with leak checking off, because allocate-once-never-free is this WAIT on the between-batches sweep slot: the switches are
runtime's design and a leak check produces a suppression list. A green sweep =ASAN_OPTIONS=detect_leaks=1 dune build @sanitize= and =FLAN_LEAKS=1 dune build @valgrind=.
therefore says nothing about who frees the newly allocating =(bytes s)=. Worth A 22-program LSan sample found no runtime leak; program leaks in map-remove and
asking on purpose one day, across the whole corpus and not one program. map-keys are fixed. Needs a decision: =(bytes s)= and =(clone slice)= with no
allocator answer a =[T]= over a heap block nothing can free (bytes-copy.flan).
Temp-allocator by default, as =i64->bytes= is, or a =(Vec T)= the caller frees?
** DONE trap_oom has no site ** DONE trap_oom has no site
CLOSED: [2026-09-25] CLOSED: [2026-09-25]

View File

@ -690,11 +690,23 @@ let compile_c ~opts ?tflags ?(warn = []) ~src ~name () =
built against, so repointing either must not serve a stale .o. *) built against, so repointing either must not serve a stale .o. *)
let tflags = match tflags with Some f -> f | None -> target_flags opts in let tflags = match tflags with Some f -> f | None -> target_flags opts in
let cc = compiler opts in let cc = compiler opts in
(* A source that includes the dyn header was compiled against it, so the
header is part of what the object depends on. Without it a change to a
struct the header declares served an object built against the old layout
— test/dyn_ops.c read a descriptor's new fields past the end of its own. *)
let header =
let needle = "flan_dyn.h" in
let n = String.length needle and m = String.length src in
let rec has i =
i + n <= m && (String.sub src i n = needle || has (i + 1))
in
if has 0 then Runtime_src.dyn_header else ""
in
let key = let key =
Digest.to_hex Digest.to_hex
(Digest.string (Digest.string
(String.concat "\000" (String.concat "\000"
[ name; src; stamp_of cc; opts.opt; [ name; src; header; stamp_of cc; opts.opt;
String.concat " " (cflags opts); String.concat " " (cflags opts);
String.concat " " tflags; String.concat " " tflags;
String.concat " " warn ])) String.concat " " warn ]))

View File

@ -118,6 +118,8 @@
(deps (deps
(alias corpus) (alias corpus)
test_sanitize.exe test_sanitize.exe
; ASAN_OPTIONS=detect_leaks=1 asks the leak question; see test_sanitize.ml.
(env_var ASAN_OPTIONS)
; The dyn runtime's C main, which is the one thing in this sweep that is not ; The dyn runtime's C main, which is the one thing in this sweep that is not
; a Flan program: flan_dyn.c has no Flan spelling yet. It is also the one ; a Flan program: flan_dyn.c has no Flan spelling yet. It is also the one
; translation unit here that frees the most, which is what makes it worth a ; translation unit here that frees the most, which is what makes it worth a
@ -149,7 +151,9 @@
test_valgrind.exe test_valgrind.exe
; The suppression file, which is all reasons and no suppressions; its own ; The suppression file, which is all reasons and no suppressions; its own
; header says why that is the finding rather than an oversight. ; header says why that is the finding rather than an oversight.
(file valgrind.supp)) (file valgrind.supp)
; FLAN_LEAKS=1 turns memcheck's leak check on; see test_valgrind.ml.
(env_var FLAN_LEAKS))
(action (run ./test_valgrind.exe))) (action (run ./test_valgrind.exe)))
; The corpus a fourth time, through the hand-written x86-64 backend, compared ; The corpus a fourth time, through the hand-written x86-64 backend, compared

View File

@ -22,7 +22,8 @@
(dotimes [i (length vs)] (print (at vs i)) (print " ")) (dotimes [i (length vs)] (print (at vs i)) (print " "))
(println "") (println "")
(free ks) (free ks)
(free vs))) (free vs))
(free m))
;; A string key, and a map that never allocated. ;; A string key, and a map that never allocated.
(let [names (map-new string i32) (let [names (map-new string i32)
none (map-new string i32)] none (map-new string i32)]

View File

@ -123,7 +123,8 @@
(print (length t)) (println "") ; 150 (print (length t)) (println "") ; 150
(match (get t 299) (Some v) (do (print v) (println "")) None (println "?")) ; 598 (match (get t 299) (Some v) (do (print v) (println "")) None (println "?")) ; 598
(print (has-key? t 298)) (println ""))) ; false (print (has-key? t 298)) (println ""))) ; false
(free-all ar)) (free-all ar)
(arena-destroy ar))
;; (7) Churn at a steady size, checked against a plain array. Keys are drawn ;; (7) Churn at a steady size, checked against a plain array. Keys are drawn
;; from 512 and the map holds about two thirds of them, so removals leave ;; from 512 and the map holds about two thirds of them, so removals leave

View File

@ -5659,7 +5659,7 @@ level "1"
(List.filter (List.filter
(fun line -> (fun line ->
contains line prefix contains line prefix
&& contains line "constant { i64, i64, ptr, i64, ptr, i64, ptr }") && contains line "constant { i64, i64, ptr, i64, ptr, i64, ptr, i64, ptr }")
(String.split_on_char '\n' ir)) (String.split_on_char '\n' ir))
in in
if n <> want then begin if n <> want then begin

View File

@ -41,15 +41,22 @@ let fail fmt = Test_support.fail fmt
let scratch = Test_support.scratch let scratch = Test_support.scratch
let supp = "valgrind.supp" let supp = "valgrind.supp"
(* Leak checking is off, and the reason is [test_sanitize.ml]'s reason for (* Leak checking is off by default, and the reason is [test_sanitize.ml]'s
detect_leaks=0 unchanged: allocate-once-never-free is this runtime's design, reason for detect_leaks=0 unchanged: allocate-once-never-free is this
not an accident — rt_args says so in its own comment, an arena hands back runtime's design — rt_args says so in its own comment, and the context
nothing before arena-destroy, and the context temp arena is made on first temp arena is made on first use and never released. FLAN_LEAKS=1 asks the
use and never released. LeakSanitizer produced a suppression list and no leak question on purpose: definite leaks only, which is memory nothing
information; memcheck would produce the same list. The question is worth points at any more, counted as errors. What an allocator holds by design is
asking on purpose one day, and this is not that run. *) still reachable and is not reported. The sanitize sweep's switch is
ASAN_OPTIONS=detect_leaks=1. *)
let leaks = Sys.getenv_opt "FLAN_LEAKS" = Some "1"
let vg_flags = let vg_flags =
[ "--leak-check=no"; "--error-exitcode=0"; "--track-origins=yes"; (if leaks then
[ "--leak-check=full"; "--show-leak-kinds=definite";
"--errors-for-leak-kinds=definite" ]
else [ "--leak-check=no" ])
@ [ "--error-exitcode=0"; "--track-origins=yes";
(* Origins are what turn "uninitialised value" into a line naming the (* Origins are what turn "uninitialised value" into a line naming the
allocation it came from. They cost roughly 2x on top of memcheck and allocation it came from. They cost roughly 2x on top of memcheck and
are worth every bit of it: without them an uninitialised-read report are worth every bit of it: without them an uninitialised-read report