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;
|
end;
|
||||||
(try Sys.remove exe with Sys_error _ -> ());
|
(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
|
(* §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
|
exit 0: a handler runs, returns normally, and has still not answered the
|
||||||
error, so the program stops and names the condition. *)
|
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"
|
refuses_src "defer in a let inside a branch"
|
||||||
"(defn g [] 0)\n(defn f [] (if true (let [x 1] (defer (g))) 0))"
|
"(defn g [] 0)\n(defn f [] (if true (let [x 1] (defer (g))) 0))"
|
||||||
"not allowed inside a branch";
|
"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
|
let signed_out = "-4\n-1\nbig is not small\nbig is large\n1\n" in
|
||||||
outputs "signedness" "programs/signedness.flan" signed_out;
|
outputs "signedness" "programs/signedness.flan" signed_out;
|
||||||
outputs ~opt:"-O0" "signedness, -O0" "programs/signedness.flan" signed_out;
|
outputs ~opt:"-O0" "signedness, -O0" "programs/signedness.flan" signed_out;
|
||||||
|
|||||||
@ -685,8 +685,13 @@ let () =
|
|||||||
back as generics. *)
|
back as generics. *)
|
||||||
rejects_check "Vec takes one type" "(defn f [x (Vec i32 i32)])"
|
rejects_check "Vec takes one type" "(defn f [x (Vec i32 i32)])"
|
||||||
~needle:"exactly one type";
|
~needle:"exactly one type";
|
||||||
rejects_check "Map is milestone 6" "(defn f [x {string i32}])"
|
(* {K V} resolves now — it is the Map type spelling, and the only one, since
|
||||||
~needle:"milestone 6";
|
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)"
|
rejects_check "Result is milestone 6" "(defn f [] (Result i32 i32) None)"
|
||||||
~needle:"milestone 6";
|
~needle:"milestone 6";
|
||||||
rejects_check "try is milestone 6" "(defn f [] i32 (try 1))"
|
rejects_check "try is milestone 6" "(defn f [] i32 (try 1))"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user