From c45447a6d4afa59dac51d2c84bb49d42fb50e454 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 16:09:27 +0700 Subject: [PATCH] The Map's tests join the suite, and the epoch trap covers its half MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit maps.flan and map-exhausted.flan as fixed-output cases, the six refusals by name, and map-stale-region.flan beside stale-region.flan. The last one is not a line in the Vec's program because the two reach the check by different routes. A Vec's operations check on the way in and stop there. A map's get goes on to call a hash and an equality function through pointers into the block, so a missing check there is not a wrong number — it is a probe loop walking released memory. It traps naming the site and exits 134, as the Vec's does. {K V} resolves now, so the test that asserted it was milestone 6 is replaced by the one that still holds: the arity, refused for the reason Vec's arity is refused, because a near-miss would otherwise resolve to a type variable and come back as generics. --- test/programs/map-stale-region.flan | 25 ++++++++++ test/test_acceptance.ml | 73 +++++++++++++++++++++++++++++ test/test_flan.ml | 9 +++- 3 files changed, 105 insertions(+), 2 deletions(-) create mode 100644 test/programs/map-stale-region.flan diff --git a/test/programs/map-stale-region.flan b/test/programs/map-stale-region.flan new file mode 100644 index 0000000..ec39ece --- /dev/null +++ b/test/programs/map-stale-region.flan @@ -0,0 +1,25 @@ +;;;; spec-memory.md, "Dev builds detect a released region" — the Map's half. +;;;; +;;;; A Map records the epoch of the allocator it was made with, exactly as a +;;;; Vec does, and free-all bumps that counter. Any operation on a container +;;;; whose recorded epoch has moved traps, naming the site. +;;;; +;;;; This is worth its own program rather than a line in stale-region.flan +;;;; because the two containers reach the check by different routes: a Vec's +;;;; every operation takes the Vec's address and checks on the way in, while a +;;;; map's get goes on to call a hash and an equality function through pointers +;;;; into a block that is no longer there. If the check were missing here, the +;;;; failure would not be a wrong number — it would be a probe loop walking +;;;; released memory. +(defn main [] i32 + (let [a (arena-new 65536)] + (let [m (map-new i32 i32 a)] + (put m 1 10) + (put m 2 20) + (match (get m 2) (Some v) (println v) None (println "missing")) + ;; The region goes. m is still in scope and still looks fine — nothing is + ;; released at scope exit and nothing marked m — which is exactly the + ;; case a static rule cannot see. + (free-all a) + (match (get m 2) (Some v) (println v) None (println "missing")))) + 0) diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 660e507..19cc859 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -581,6 +581,25 @@ let () = end; (try Sys.remove exe with Sys_error _ -> ()); + (* The same trap on the Map's side, and it is not the same code path: a + Vec's operations check on the way in and stop there, while a map's get + goes on to call a hash and an equality function through pointers into + the block. A missing check here is not a wrong number, it is a probe + loop walking released memory. *) + let exe = compile "programs/map-stale-region.flan" in + let code, text = run exe None in + if code <> 134 || not (contains text "programs/map-stale-region.flan:") + || not (contains text "allocator was released") + || not (contains text "20") + then begin + incr failures; + Printf.printf + "FAIL a map used after its region was released\n\ + \ got: %S (exit %d)\n wanted: exit 134, naming the site\n" + text code + end; + (try Sys.remove exe with Sys_error _ -> ()); + (* §2's other half, which cannot be an [outputs] case because it does not exit 0: a handler runs, returns normally, and has still not answered the error, so the program stops and names the condition. *) @@ -1531,6 +1550,60 @@ ERR@7 unexpected token: not the kind the caller was reading refuses_src "defer in a let inside a branch" "(defn g [] 0)\n(defn f [] (if true (let [x 1] (defer (g))) 0))" "not allowed inside a branch"; + + (* ── (Map K V), spec-memory.md step 4 ────────────────────────── + + Odin's map: open-addressed Robin Hood hashing at a 75% load factor with + cache-line cell packing. maps.flan is seven claims, each one a plausible + wrong version gets wrong, and the numbers differ per failure. + + The two worth naming, because nothing else in the suite would catch + them. A struct key is hashed *field by field*, never bytewise, because a + struct's padding bytes are indeterminate — hashing them makes two equal + keys hash differently and the entry unfindable, which shows up here as a + wrong count rather than a crash. And a grow rehashes against a fresh + seed, because the seed is derived from the block address; carrying the + old hashes across a grow puts every entry in a slot nothing will probe. + 2000 entries is eight grows and then every one of them read back. *) + let maps_out = + "2000\n0\n1600\n709\ntrue\nfalse\n3\n20\nfalse\n2\n3\nfalse\n\ + 100\n999\n2\n1\n3\n500\n998\n" + in + outputs "maps" "programs/maps.flan" maps_out; + outputs ~opt:"-O0" "maps, -O0" "programs/maps.flan" maps_out; + + (* The allocation-failure rule is one rule over every allocating operation, + so it has to hold for map-new, put, reserve and clone as it does for the + Vec's four. A map is the harder case: its growth allocates a 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. *) + let map_exhausted_out = "300\n0\ntrue\ntrue\ntrue\n0\n2\n2\ntrue\n" in + outputs "map StorageExhausted and retry" "programs/map-exhausted.flan" + map_exhausted_out; + + (* The refusals, each by name. A float key is not a milestone question — + NaN is not equal to itself and 0.0 and -0.0 are equal while differing + bytewise, so there is no equality for a map to hash. A move-only value + is the refusal (Vec (Vec T)) already carries, for the identical reason. + Unit as a value is refused rather than dividing a cache line by zero, + and it is named because it is the natural spelling of a set. *) + refuses_src "a float is not a map key" + "(defn f [m (Map f32 i32)] 0)" "is not a map key"; + refuses_src "a Ptr is not a map key" + "(defn f [m (Map (Ptr i32) i32)] 0)" "hash an address"; + refuses_src "a map value may not own storage" + "(defn f [m (Map i32 (Vec i32))] 0)" "holds a move-only value"; + refuses_src "a map value may not be Unit" + "(defn f [m (Map i32 Unit)] 0)" "cannot be Unit"; + refuses_src "map-new with nothing to say what it maps" + "(defn main [] i32 (let [m (map-new)] (free m)) 0)" + "nothing here says what (map-new) maps"; + (* A map is move-only like a Vec, and the refusal names the type that was + moved rather than saying "a Vec" whatever it was. *) + refuses_src "a map used after it was moved" + "(defn main [] i32 (let [m (map-new i32 i32)] (free m) (put m 1 2)) 0)" + "cannot be used again"; let signed_out = "-4\n-1\nbig is not small\nbig is large\n1\n" in outputs "signedness" "programs/signedness.flan" signed_out; outputs ~opt:"-O0" "signedness, -O0" "programs/signedness.flan" signed_out; diff --git a/test/test_flan.ml b/test/test_flan.ml index 191f879..f98bac0 100644 --- a/test/test_flan.ml +++ b/test/test_flan.ml @@ -685,8 +685,13 @@ let () = back as generics. *) rejects_check "Vec takes one type" "(defn f [x (Vec i32 i32)])" ~needle:"exactly one type"; - rejects_check "Map is milestone 6" "(defn f [x {string i32}])" - ~needle:"milestone 6"; + (* {K V} resolves now — it is the Map type spelling, and the only one, since + a bare map form in expression position is a struct literal's field list. + What is still refused is the arity, for the same reason Vec's is: a + near-miss would otherwise resolve to a type variable and come back as + generics. *) + rejects_check "Map takes two types" "(defn f [x (Map i32)])" + ~needle:"exactly two types"; rejects_check "Result is milestone 6" "(defn f [] (Result i32 i32) None)" ~needle:"milestone 6"; rejects_check "try is milestone 6" "(defn f [] i32 (try 1))"