From 8a254f5006fe1470b0ce622e12e6990343fcc98b Mon Sep 17 00:00:00 2001 From: dg1sbg Date: Sat, 1 Aug 2026 16:27:06 +0200 Subject: [PATCH] tests: build weak tables in a function that returns before collecting The four weakness tests in hash-tables0.lisp allocate throwaway (list nil) temporaries in the same frame that then calls garbage-collect, and assert an exact hash-table-count afterwards. Boehm scans the stack conservatively, so a stale word left in that frame or in a register can still reference one of the temporaries and keep its weak entry alive; the count comes out one too high and the test fails. This is not a collector defect. A conservative collector promises never to collect live data, not to always collect dead data, and the comment above these tests already acknowledges they are stricter than that. The tests create the stale reference themselves by building and collecting in the same frame. Move construction into a builder that RETURNS first, so the frame holding the temporaries is popped before the collection. The assertion is unchanged: one collection, exact count. Measured on x86-64 Linux (LLVM 18, boehmprecise, bytecode) by calling the test body directly under 500 frames of recursion, 1000 trials per arm. The rate depends on code layout and varies between builds: across three builds the current form failed at 16.5%, 5.4% and 0.1%. The builder form failed 0 times in 2000 trials spanning the 5.4% and 0.1% builds, where about 55 failures would otherwise be expected. Non-vacuity checked: an entry whose key and value are both held in globals still yields a count of 2 and still fails, so a genuine leak is not masked. Previously submitted as #1815, which also retried the collection. The retry proves unnecessary and is omitted, so the test asserts exactly what it does today. Refs #1814. Also drop weak-key-and-value-weakness from *expected-failures*. 2ba8b2ab8 listed both weakness tests there after concluding the key-and-value one is "basically irregular: sometimes the entry gets cleaned up and sometimes it doesn't, even after a couple rounds of garbage collection". That is exactly the flake this commit removes, so leaving the entry would report the test as an unexpected success. weak-key-or-value-weakness stays listed: on boehm key-or-value tables are effectively strong, which is a collector limitation this change does not address. --- src/lisp/regression-tests/hash-tables0.lisp | 65 +++++++++++-------- .../set-unexpected-failures.lisp | 4 +- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/src/lisp/regression-tests/hash-tables0.lisp b/src/lisp/regression-tests/hash-tables0.lisp index a8bfd63728..d771184cf8 100644 --- a/src/lisp/regression-tests/hash-tables0.lisp +++ b/src/lisp/regression-tests/hash-tables0.lisp @@ -127,40 +127,51 @@ ;;; These tests are pretty strict - they want the garbage to actually be ;;; collected by that garbage-collect call, which may not be the case if we -;;; ever get a more relaxed collector (generational or something) +;;; ever get a more relaxed collector (generational or something). +;;; +;;; BUILDER populates the table and then RETURNS, so the frame holding the dead +;;; temporaries is popped before we collect. Building and collecting in one frame +;;; lets Boehm's conservative stack scan see a stale reference to a temporary and +;;; keep its entry alive, which is what made these tests flaky. The assertion is +;;; unchanged: still one collection, still an exact count. +(defun weak-count (builder) + (let ((table (funcall builder))) + (gctools:garbage-collect) + (hash-table-count table))) + (test weak-key-weakness - (let ((table (make-hash-table :weakness :key))) - (setf (gethash (list 37) table) :value - (gethash :key table) (list nil) - (gethash :key2 table) (list nil)) - (gctools:garbage-collect) - (hash-table-count table)) + (weak-count (lambda () + (let ((table (make-hash-table :weakness :key))) + (setf (gethash (list 37) table) :value + (gethash :key table) (list nil) + (gethash :key2 table) (list nil)) + table))) (2)) (test weak-value-weakness - (let ((table (make-hash-table :weakness :value))) - (setf (gethash :key table) (list 37) - (gethash (list nil) table) :value - (gethash (list 18) table) :value) - (gctools:garbage-collect) - (hash-table-count table)) + (weak-count (lambda () + (let ((table (make-hash-table :weakness :value))) + (setf (gethash :key table) (list 37) + (gethash (list nil) table) :value + (gethash (list 18) table) :value) + table))) (2)) (test weak-key-and-value-weakness - (let ((table (make-hash-table :weakness :key-and-value))) - (setf (gethash (list nil) table) :value - (gethash :key table) (list nil) - (gethash (list nil) table) (list nil) - (gethash :key2 table) :value) - (gctools:garbage-collect) - (hash-table-count table)) + (weak-count (lambda () + (let ((table (make-hash-table :weakness :key-and-value))) + (setf (gethash (list nil) table) :value + (gethash :key table) (list nil) + (gethash (list nil) table) (list nil) + (gethash :key2 table) :value) + table))) (1)) (test weak-key-or-value-weakness - (let ((table (make-hash-table :weakness :key-or-value))) - (setf (gethash (list nil) table) :value - (gethash :key table) (list nil) - (gethash (list nil) table) (list nil) - (gethash :key2 table) :value) - (gctools:garbage-collect) - (hash-table-count table)) + (weak-count (lambda () + (let ((table (make-hash-table :weakness :key-or-value))) + (setf (gethash (list nil) table) :value + (gethash :key table) (list nil) + (gethash (list nil) table) (list nil) + (gethash :key2 table) :value) + table))) (3)) (test equalp-hash-table-1 diff --git a/src/lisp/regression-tests/set-unexpected-failures.lisp b/src/lisp/regression-tests/set-unexpected-failures.lisp index 4c0d849d9a..84287c1239 100644 --- a/src/lisp/regression-tests/set-unexpected-failures.lisp +++ b/src/lisp/regression-tests/set-unexpected-failures.lisp @@ -8,7 +8,5 @@ ;; include-level-2a include-level-2b include-level-3 ;;; a problem for sbcl x-compiling frame-function frame-locals - ;; these don't work well on boehm. In particular - ;; key-or-value tables are effectively strong. - #+use-boehm weak-key-and-value-weakness + ;; on boehm key-or-value tables are effectively strong. #+use-boehm weak-key-or-value-weakness))