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:
parent
6e29c19097
commit
a615e4ecfc
12
TODO.org
12
TODO.org
@ -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]
|
||||||
|
|||||||
14
lib/build.ml
14
lib/build.ml
@ -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 ]))
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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)]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user