flan/test/programs/dyn-map.flan
Joseph Ferano ab82e46119 Dyn maps, keywords and nil land: milestone 2's first item
The dyn runtime gets a map object and an interned keyword, alongside the
vec it already had. {:a 1 :b s} is a map literal wherever a struct
literal isn't — the parser tells the two apart by whether the first form
in the braces is a .field symbol — and a bracket literal builds the
runtime's own vec rather than a typed array wherever a dyn is wanted, which
is what lets a map literal's values nest arrays and maps freely. get, put,
len and has-key? all learn a dyn-map arm alongside the typed-map one they
already had, and (keyword s) builds the same interned value a :foo literal
does, for a name that only exists at run time. nil is now a literal, the
dyn absence value that get answers for a key a map does not hold.

On the runtime side, flan_dyn.c gets an OBJ_MAP that shares the vec's
storage arm and doubles its accounting, a linear-scan intern table for
keywords that makes equality an identity compare, and structural map
equality by lookup rather than position. The marker traces a map's
interleaved keys and values the same way it already traced a vec.

edn/read and its callers move off the old (Option Value) union entirely:
a document is plain dyn now, sets are dyn maps to true, and arena-edn.flan
is retired along with the union it demonstrated. The acceptance suite's
edn-read and json rows were recaptured against the new shape, and a new
dyn-map.flan program exercises the map and keyword operations end to end,
including a 200k-iteration churn loop against a rooted map that runs
GC for real, across the LLVM, -O0 and x86 rows, and under the sanitizer.

Keywords are dyn everywhere an enum isn't expected, which changed what a
couple of existing checker tests actually see refused; both were updated
to the sentence the checker gives now rather than the one it used to.
2026-09-19 19:44:56 +07:00

71 lines
2.7 KiB
Plaintext

;;;; Dyn maps and keywords: the literal, the operations, the printing, and a
;;;; collection loop that outruns the GC floor.
;;;;
;;;; Every value here is dyn. {:a 1} is a heap map the collector traces; :a is
;;;; an interned keyword, so two spellings of one name are the same word and
;;;; equality never reads the bytes; [1 2] where a dyn is wanted is the
;;;; runtime's own vec. The printed forms are the dyn renderer's — a space
;;;; before every element, strings quoted when nested — pinned here across
;;;; both backends the way test/dyn_ops.c pins them from C.
;; A dyn global: rooted once at startup, so what main stores in it survives
;; every collection the churn loop below causes.
(defvar config dyn)
(defn main [] i32
;; The literal, and what it prints as.
(let [m {:a 1 :b "two" :xs [1 2 3] :inner {:c 2.5}}]
(println m)
(println (len m))
(println (get m :a))
(println (get m :b))
(println (get m :xs))
(println (get (get m :inner) :c))
;; Absence is nil — an answer, not a trap — and has-key? is the question
;; that stays askable when nil might also be stored.
(println (get m :missing))
(println (= (get m :missing) nil))
(println (has-key? m :a))
(println (has-key? m :missing))
(put m :flag nil)
(println (get m :flag))
(println (has-key? m :flag))
;; put replaces an equal key's value in place; the length holds still.
(put m :a 99)
(println (get m :a))
(println (len m))
;; Keywords: identity equality, printing, and the runtime constructor —
;; (keyword "a") has to be the same word as the literal :a.
(println (= :a :a))
(println (= :a :b))
(println (= :a "a"))
(println (= (keyword "a") :a))
(println :standalone)
;; Keys are whole values compared structurally: a text and a keyword are
;; two keys, and a vec of numbers can key a map.
(put m "a" "text key")
(println (len m))
(println (get m "a"))
(put m [1 2] "vec key")
(println (get m [1 2]))
;; Maps compare structurally, by lookup and not by insertion order.
(println (= {:x 1 :y 2} {:y 2 :x 1}))
(println (= {:x 1} {:x 2}))
(println (= {:x 1} {:x 1 :y 2})))
;; The churn: enough map, vec and text allocation to pass the 1 MiB floor
;; many times over, against one live map held in a rooted global. A marker
;; that lost track of a map's keys or values frees something live, and the
;; sum at the end comes out wrong — or ASan speaks, in the sanitize sweep.
(set config {:total 0})
(dotimes [i 200000]
(let [row {:i 1 :s "forty-seven bytes of text to fatten each row" :v [1 2 3]}]
(put config :total (+ (get config :total) (len row)))))
(println (get config :total))
0)