diff --git a/test/programs/map-exhausted.flan b/test/programs/map-exhausted.flan new file mode 100644 index 0000000..4ca6e2e --- /dev/null +++ b/test/programs/map-exhausted.flan @@ -0,0 +1,82 @@ +;;;; StorageExhausted and retry, over a Map — spec-memory.md, "Allocation +;;;; failure". The rule is one rule over *every* allocating operation, so it has +;;;; to hold for map-new, put, reserve and clone exactly as exhausted.flan shows +;;;; it holding for vec-new, push, reserve and clone. put stays Unit, clone +;;;; stays the container, and no signature anywhere grows a Result. +;;;; +;;;; A map is the harder case of the two, and that is why it gets its own +;;;; program. A Vec's failing allocation leaves the Vec untouched. A map's +;;;; growth allocates a whole new block, rehashes into it and only then +;;;; releases the old one — so a failure partway must leave the map exactly as +;;;; it was, or the retry re-attempts against a half-moved map. + +;; Globals, because a handler cannot see the locals of the function that +;; established it. +(defvar tight Allocator) +(defvar failures i64) +(defvar last-bytes i64) +(defvar same-allocator bool) + +(defn main [] i32 + (set tight (heap-allocator)) + ;; Enough for the smallest block and not for the one after it. A map's block + ;; is keys, values, hashes and scratch, each rounded to a cache line, so it + ;; is a few hundred bytes at eight slots — the ceiling has to be low enough + ;; to be hit and high enough that the first block fits after one raise. + (set-alloc-budget tight 64) + + (handler-bind + [(StorageExhausted [c] + (set failures (+ failures 1)) + (set last-bytes (.bytes c)) + (set same-allocator (= (.allocator c) (alloc-id tight))) + ;; Raise the ceiling and re-attempt the same request. The map is + ;; untouched and its allocator is unchanged, which is why this can + ;; succeed — releasing the region instead would invalidate the map, + ;; which is what the epoch check catches and is its own program. + (set-alloc-budget tight (* 4 (alloc-budget tight))) + (invoke-restart 'retry))] + (let [m (map-new i32 i64 tight)] + ;; Several grows, each one a fresh block rehashed into. A put that fails + ;; puts nothing, and the retry puts exactly once — so no entry is lost + ;; and none is doubled. + (dotimes [i 300] (put m i (* (i64 i) 7))) + (println (len m)) ; 300 + (let [bad 0] + (dotimes [i 300] + (match (get m i) + (Some v) (if (not (= v (* (i64 i) 7))) (set bad (+ bad 1))) + None (set bad (+ bad 1)))) + (println bad)) ; 0 + (free m))) + + (println (> failures 1)) ; true + (println (> last-bytes 0)) ; true + (println same-allocator) ; true + + ;; reserve asks for the whole block at once, and clone asks the new allocator + ;; for one big enough to hold the source — both are allocating operations and + ;; neither returns an error. + (set-alloc-budget tight 64) + (set failures 0) + (handler-bind + [(StorageExhausted [c] + (set failures (+ failures 1)) + (set-alloc-budget tight (* 8 (alloc-budget tight))) + (invoke-restart 'retry))] + (let [m (map-new string i32 tight)] + (reserve m 200) + (println (len m)) ; 0 + (put m "a" 1) + (put m "b" 2) + (let [w (clone m)] + (println (len w)) ; 2 + (match (get w "b") + (Some v) (println v) ; 2 + None (println "missing")) + (free w)) + (free m))) + (println (> failures 0)) ; true + + (set-alloc-budget tight 0) + 0)