Cover a dev build and a map that leaves its let

Two gaps nothing in the suite reached.

A dev build, because the hash and equality pair emitted for a struct key
is a function nobody wrote, and the only other inhabitant of the lifted
list — a handler-bind clause — carries a parent this one cannot: the
pair is shared by every function that maps that key type, so it has no
single parent. A dev build puts every body behind an indirection cell
and is the build that would notice. It does not; maps.flan answers the
same nineteen ways at --dev as it does at -O2 and -O0.

And a map crossing a function boundary in both directions. Everything
else in the file lives and dies inside one let, so nothing would have
noticed if the 48-byte header travelled wrongly by value while every
runtime operation takes its address. Returning one and passing one are
both moves, which is the rule a Vec already follows — verified against a
Vec rather than assumed, since a refusal that fired for the wrong reason
would look the same.

has-key? is flagged in BUILT.md as what it is: an addition, not
something spec-memory.md names.
This commit is contained in:
Joseph Ferano 2026-09-12 16:34:25 +07:00
parent b2059520ab
commit ea24461107
3 changed files with 36 additions and 2 deletions

View File

@ -1719,9 +1719,14 @@ calls per field, which has no channel to hand on.
| `(map-new)` `(map-new K V)` `(map-new a)` `(map-new K V a)` | a new map; the pair may be omitted where the context says |
| `(put m k v)` | upsert, `Unit` |
| `(get m k)` | `(Option V)` — absence is `None` |
| `(has-key? m k)` | `bool`, copying no value |
| `(has-key? m k)` | `bool`, copying no value **an addition; the spec does not name it** |
| `(len m)` `(reserve m n)` `(clone m)` `(clone m a)` `(free m)` | extended, not duplicated |
`has-key?` is **not in `spec-memory.md`** and is an addition, flagged because everything else here is the spec's.
`(get m k)` answers the same question, but through an `Option` the caller then has to match, and the common use is a
condition. It copies no value, which is also why it is not just `get` with the result thrown away. The `?` suffix
follows `can-free?`.
`len`, `reserve`, `clone` and `free` were **extended rather than given map-shaped names of their own**, which is what
`at` and `len` already did for `Vec`: one question, one word. `reserve`'s `n` is entries, not slots — the runtime sizes
the block so `n` still sits under the load factor, which is the only reading of "room for n" that does not reallocate

View File

@ -8,6 +8,25 @@
(defstruct Named [tag string n i32])
(defenum Suit [hearts 0 spades 1 clubs 2])
;;;; (8) A map crosses a function boundary in both directions. Returning one
;;;; and passing one are both *moves* — the same rule a Vec follows, so the
;;;; binding is dead afterwards — and the 48-byte header travels by value while
;;;; every runtime operation takes its address. Nothing else in this file
;;;; leaves a single let, so nothing else would notice if it did not.
(defstruct Cell2 [x i32 y i32])
(defn make-grid [] (Map Cell2 i32)
(let [m (map-new Cell2 i32)]
(put m (Cell2 {.x 1 .y 2}) 12)
(put m (Cell2 {.x 3 .y 4}) 34)
m))
;; Takes the map, which is a move: this owns it now, and frees it.
(defn consume-grid [m (Map Cell2 i32)] i32
(let [n (len m)]
(free m)
n))
(defn main [] i32
;; (1) An integer key past several grows. The map starts at 8 slots, so 2000
;; entries is eight reallocations, and every one of them rehashes against a
@ -103,4 +122,8 @@
(print (len t)) (println "") ; 500
(match (get t 499) (Some v) (do (print v) (println "")) None (println "?")))) ; 998
(free-all ar))
(let [g (make-grid)]
(print (len g)) (println "") ; 2
(print (consume-grid g)) (println "")) ; 2
0)

View File

@ -1567,10 +1567,16 @@ ERR@7 unexpected token: not the kind the caller was reading
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"
100\n999\n2\n1\n3\n500\n998\n2\n2\n"
in
outputs "maps" "programs/maps.flan" maps_out;
outputs ~opt:"-O0" "maps, -O0" "programs/maps.flan" maps_out;
(* A dev build, because the hash and equality pair emitted for a struct key
is a function nobody wrote and the only other inhabitant of that list
a lifted handler clause carries a parent this one cannot: the pair is
shared by every function that maps that key type. A dev build puts every
body behind an indirection cell, so it is the build that would notice. *)
outputs ~dev:true "maps, dev" "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