flan/test/programs/arena-edn.flan
Joseph Ferano a0f37e72a2 The ! suffix retires: a mutator is named for what it does, not marked
The !-means-mutates convention distinguished nothing — there is no
immutable counterpart to contrast with — so every mutating name drops
the mark: sort, sort-by, sort-bytes, swap, reverse, append, append-i64,
append-f64, encode-rune, split-next, map-remove, map-next, and the test
helpers beside them. Two could not simply shed it: map! is map-in-place,
because map is the into transform's word and means the non-mutating
thing; put! is put-at, because put is the Map builtin. The ?-means-asks
convention stays. Dated records keep the old spellings; watch.clj's
reset-spies! and the other Clojure names are not ours to rename.
2026-09-19 05:21:02 +07:00

128 lines
5.2 KiB
Plaintext

;;;; An EDN document read into a dynamic value, against an arena.
;;;;
;;;; This is the other half of programs/edn.flan. That one reads a document
;;;; whose shape is known into a struct, by hand, which is what the compiler's
;;;; (read-edn Enemy bytes) will emit. This one is what a reader handed *no*
;;;; target type has to answer with: a data type naming itself through a
;;;; (Vec Value) and a (Map string Value), holding whatever was in the file.
;;;;
;;;; ── The allocator story, which is the point of the program ───────────
;;;;
;;;; edn/read takes no allocator and names none. It does not need to:
;;;; spec-memory.md puts the allocator in the calling convention, so every
;;;; (vec-new) and (map-new) inside it takes the *context*, and the caller
;;;; chooses the tier with (with-allocator ...) around the call. An explicit
;;;; allocator at a construction site overrides that, which is how a reader
;;;; would take one as a parameter if it wanted to — but the existing idiom
;;;; already does the job, so there is no new machinery here and none needed.
;;;;
;;;; The tier has to be a region and not the heap, and that is enforced rather
;;;; than documented: a (Vec Value) whose elements own storage traps at its
;;;; construction against any allocator that can free one block. See
;;;; programs/arena-region.flan.
;;;;
;;;; ── What the region buys, said plainly ───────────────────────────────
;;;;
;;;; The document below is four levels deep and every level allocates. There is
;;;; no teardown anywhere in this file: no drop, no destructor, no recursive
;;;; free, not even a (free) call. One (free-all frame) at the bottom of main
;;;; releases every Vec block, every Map block and every entry in them, because
;;;; they all came out of the same region.
;;;;
;;;; Odin's core:encoding/json ships a hand-written recursive destroy_value in
;;;; the *library* for the heap case, and names parsing against temp_allocator
;;;; and calling free_all as the idiomatic alternative. This is that
;;;; alternative, and it needs nothing from the language that was not already
;;;; there.
;;;;
;;;; ── The reader moved, and what is left here ──────────────────────────
;;;;
;;;; read-value used to be written out in this file. It is vendor/edn's now, as
;;;; (edn/read bytes), and what this program keeps is the half that was always
;;;; the demonstration: the walk back over a document nobody declared a type
;;;; for, and the one free-all that ends it. The strings are the package's own
;;;; copies in the region now rather than views into `doc`, which is why there
;;;; is nothing left here saying which parts of the value outlive the release.
(import edn "vendor:edn")
(defvar frame Allocator)
(defn count-leaves [v edn/Value] i32
(match v
(List items)
(let [n 0]
(dotimes [i (len items)]
(set n (+ n (count-leaves (at items i)))))
n)
(Set items)
(let [n 0]
(dotimes [i (len items)]
(set n (+ n (count-leaves (at items i)))))
n)
;; map-next fills an out-parameter with a copy of the value's bytes,
;; which for a Value holding a container is a second header over the same
;; block. In a region that is an alias and not a second owner — nothing
;; here owns anything, the arena does — so walking a map is the ordinary
;; iteration and needs no accessor of its own.
(Table entries)
(let [n 0
cur (i64 0)
k ""
v edn/Value.Nil]
(while (map-next entries (addr cur) (addr k) (addr v))
(set n (+ n (count-leaves v))))
n)
_ 1))
(defn sum-ints [v edn/Value] i64
(match v
(Int n) n
(List items)
(let [t (i64 0)]
(dotimes [i (len items)]
(set t (+ t (sum-ints (at items i)))))
t)
(Table entries)
(match (get entries "xs") (Some x) (sum-ints x) None (i64 0))
_ (i64 0)))
(defn describe [v edn/Value] string
(match v
Nil "nil" (Bool _b) "bool" (Int _n) "int" (Float _x) "float"
(Text _s) "string" (Key _s) "keyword" (List _i) "vector"
(Set _i) "set" (Table _e) "map"))
(defconst doc
"{:name \"level-1\"
:xs [1 2 3]
:spawns [{:kind :grunt :at [10 20]}
{:kind :boss :at [30 40]}]
:gravity 9.8
:looping true}")
(defn main [] i32
(set frame (arena-new 65536))
(with-allocator frame
;; None is the malformed document, and it cannot happen for a literal that
;; is right here — but reading it back out of the Option is what makes the
;; refusal visible at the call site instead of arriving as a Nil that looks
;; like data.
(match (edn/read (bytes doc))
(Some v)
(do
(println (describe v))
(println (count-leaves v))
(println (sum-ints v))
(match v
(Table entries)
(match (get entries "name")
(Some n) (println (describe n))
None (println "missing"))
_ (println "not a map")))
None (println "malformed")))
;; The whole document, in one operation and with no per-element teardown.
(free-all frame)
(arena-destroy frame)
0)