The Map's tests join the suite, and the epoch trap covers its half
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.
This commit is contained in:
parent
1bc5161ee2
commit
c45447a6d4
25
test/programs/map-stale-region.flan
Normal file
25
test/programs/map-stale-region.flan
Normal file
@ -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)
|
||||
@ -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;
|
||||
|
||||
@ -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))"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user